agen-vektor 0.3.15 → 0.3.16

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.md CHANGED
@@ -1,90 +1,84 @@
1
- ```
2
- ████████████████████████████████████████████████
3
- █ █
4
- █ V E C T O R A G E N T █
5
- █ █
6
- ████████████████████████████████████████████████
7
- ```
1
+ <div align="center">
2
+ <img src="assets/banner.svg" alt="VectorHead — will run commands on your behalf to help you build." width="720"/>
3
+ </div>
8
4
 
9
5
  <div align="center">
10
6
 
11
7
  **AI Coding Agent for Linux & Termux** — a terminal-native coding agent that inspects your project, plans, edits code, runs commands and tests, and iterates until the task is done.
12
8
 
13
- ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20Termux-9EFC62?style=for-the-badge&labelColor=0b0f14)
14
- ![Node](https://img.shields.io/badge/node-20%2B-339933?style=for-the-badge&labelColor=0b0f14)
15
- ![License](https://img.shields.io/badge/license-MIT-3B82F6?style=for-the-badge&labelColor=0b0f14)
9
+ [![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20Termux-9EFC62?style=for-the-badge&labelColor=0b0f14)](https://www.npmjs.com/package/agen-vektor)
10
+ [![Node](https://img.shields.io/badge/node-20%2B-339933?style=for-the-badge&labelColor=0b0f14)](https://nodejs.org)
11
+ [![npm](https://img.shields.io/npm/v/agen-vektor?style=for-the-badge&color=FF8534&labelColor=0b0f14)](https://www.npmjs.com/package/agen-vektor)
12
+ [![License](https://img.shields.io/badge/license-MIT-3B82F6?style=for-the-badge&labelColor=0b0f14)](LICENSE)
16
13
  [![Tests](https://img.shields.io/github/actions/workflow/status/clickmamaheti-prog/vector-agent/ci.yml?style=for-the-badge&label=tests&labelColor=0b0f14)](https://github.com/clickmamaheti-prog/vector-agent/actions/workflows/ci.yml)
17
14
 
18
- </div>
19
-
20
- ---
15
+ *Zero runtime dependencies · Free tier included · No telemetry*
21
16
 
22
- ```
23
- VectorHead ● Ready deepseek-ai/DeepSeek-V4-Flash
24
- [09:41]
25
- Perbaiki error authentication pada project ini
26
-
27
- # Cek Auth ← kuning (heading H1–H6, Freebuff palette)
28
- VectorHead akan memeriksa file auth, memperbaiki, lalu menjalankan test.
29
- [09:42]
30
- $ npm test
31
- ... (2 more lines above)
32
- 3 tests passed
33
- ╭─────────────────────────────────────────────────────────────────────────────╮
34
- │ │
35
- │ ▍ Enter a coding task or / for commands │
36
- │ │
37
- ╰─────────────────────────────────────────────────────────────────────────────╯
38
- ● Ready ● ask
39
- ```
17
+ </div>
40
18
 
41
- A Freebuff-style terminal UI with the EXACT Freebuff dark palette (`#9EFC62`
42
- green accents, markdown headings H1–H6 all yellow `#facc15`, inline code
43
- BOLD orange `#FF8534` on a `#374151` chip, links blue `#3B82F6`, fenced
44
- code `// lang` header `#5b647a` on a `#374151` slab, plain prose — quoted
45
- from [CodebuffAI/freebuff](https://github.com/CodebuffAI/freebuff)
46
- theme-system.ts + markdown-renderer.tsx), a compact agent-activity strip
47
- (⚙ tool calls), and a rounded input box with a block cursor — all rendered
48
- incrementally with no flicker.
19
+ *A terminal UI that stays out of your way — chat, tool activity, and code
20
+ render cleanly right where you work.*
49
21
 
50
22
  ## Contents
51
23
 
52
- - [Features](#features) · [Installation](#installation) · [Quick start](#quick-start) · [Command line](#command-line) · [TUI commands](#tui-commands)
53
- - [Provider configuration](#provider-configuration) · [Themes](#themes) · [Security model](#security-model) · [Privacy](#privacy) · [Architecture](#architecture) · [Development](#development)
54
- - [Testing](#testing) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [License](#license)
24
+ - [Features](#features) · [Free tier](#free-tier-zero-config) · [Installation](#installation) · [Quick start](#quick-start) · [Command line](#command-line) · [TUI commands](#tui-commands)
25
+ - [Provider configuration](#provider-configuration) · [Skills](#skills) · [Agent files](#agent-files) · [Themes](#themes) · [Security model](#security-model) · [Privacy](#privacy)
26
+ - [Architecture](#architecture) · [Development](#development) · [Testing](#testing) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [License](#license)
55
27
 
56
28
  ## Features
57
29
 
58
30
  - **Interactive TUI** — chat, agent status, tool execution, diff viewer, input. Keyboard driven, mouse-aware, works on 80×24 terminals and survives resize. Scroll the chat with PgUp/PgDn, the mouse wheel, or ↑/↓ while the input line is empty.
59
31
  - **Agent loop** — plan → inspect → tool call → result → review → fix → retry until done (with an iteration cap).
60
32
  - **Tools** — read/write/edit files, list directories, search files, run shell commands, git, fetch web pages, and **live web search out of the box** (no key needed — searches relay through the VectorHead gateway; bring your own Exa/Serper/Brave key to use your own quota).
33
+ - **Skills** — 14 bundled, agentskills.io-format skills with progressive disclosure (`list_skills` / `read_skill` tools, `/skills` in the TUI); drop your own into `~/.vector/skills/` or `.agents/skills/`.
34
+ - **Agent files** — SOUL.md / MEMORY.md / USER.md / AGENTS.md auto-seeded in `~/.vector/` and injected into the system prompt (identity, notes, preferences, project rules).
61
35
  - **Security first** — command policy classifies every shell command (SAFE / ASK / DANGEROUS / BLOCKED), permission prompts before risky actions, `--yolo` mode with warnings, and API keys are never printed or persisted to sessions.
62
- - **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint.
36
+ - **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint — unlimited side-by-side providers with per-provider keys, models and headers.
63
37
  - **Sessions & memory** — conversations persist to `~/.vector/sessions/`; a relaunch in the same project resumes the conversation automatically (agent keeps the last replies in context), `/continue` resumes any session, and `Ctrl+L`/`/clear` starts fresh.
64
38
  - **Context management** — only relevant context is sent to the model, with compaction (`/compact`).
65
39
 
40
+ ## Free tier (zero config)
41
+
42
+ Install and chat — no account, no API key:
43
+
44
+ ```bash
45
+ npm install -g agen-vektor
46
+ vector
47
+ ```
48
+
49
+ The default provider is **`vectorhead-free`** (model `glm-5.3-flash`, 40 free
50
+ models via `/model`). The shared provider key stays server-side; web search
51
+ works through the same gateway out of the box. Bring your own provider key at
52
+ any time via `/connect`.
53
+
66
54
  ## Installation
67
55
 
68
56
  Requirements: **Node.js 20+** and **npm** (Termux: `pkg install nodejs`).
69
57
 
70
- ### From source
58
+ ### One-shot installer (recommended)
71
59
 
72
- Clone this repo, then:
60
+ No Node.js/npm yet? The installer sets up Node 20+ & npm (apt on Debian/Ubuntu,
61
+ `pkg` on Termux) and then installs VectorHead for you:
73
62
 
74
63
  ```bash
75
- npm install
76
- npm run build
77
- npm link # makes `vector` available on your PATH
64
+ bash <(curl -fsSL https://raw.githubusercontent.com/clickmamaheti-prog/vector-agent/main/install.sh)
78
65
  ```
79
66
 
67
+ ### From npm
68
+
80
69
  ```bash
70
+ npm install -g agen-vektor
81
71
  vector
82
72
  ```
83
73
 
84
- ### From npm
74
+ ### From source
85
75
 
86
76
  ```bash
87
- npm install -g agen-vektor
77
+ git clone https://github.com/clickmamaheti-prog/vector-agent
78
+ cd vector-agent
79
+ npm install
80
+ npm run build
81
+ npm link # makes `vector` available on your PATH
88
82
  vector
89
83
  ```
90
84
 
@@ -127,22 +121,29 @@ vector
127
121
  | `vector --yolo` | Skip permission prompts for ask-level actions (with warning) |
128
122
  | `vector --version` / `--help` | Version / help |
129
123
 
124
+ Generic environment variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_THEME`, `VECTOR_HOME`, `VECTOR_AUTO_APPROVE` (=1 auto-approves ASK-level permission prompts, for automated runs/tests; dangerous actions still prompt), `VECTOR_ASCII` (=1 forces ASCII safe mode — glyphs map to 1-column ASCII so CJK/Termux fonts never tear the layout; auto-on for zh/ja/ko locales and Termux, `=0` opts out on desktop).
125
+
130
126
  ## TUI commands
131
127
 
132
- Type `/` inside VectorHead:
128
+ Type `/` inside VectorHead — suggestions filter as you type:
133
129
 
134
130
  | Command | Description |
135
131
  |---|---|
136
- | `/model` | Select a model |
137
- | `/theme` | Select a theme (OpenCode-style) |
138
- | `/provider` | Select a provider |
132
+ | `/help` | Show help — `/help all` lists every command |
133
+ | `/connect` | Configure provider: pick built-in / add custom API (`/provider`, `/setup` are aliases) |
139
134
  | `/provider-remove` | Remove the stored provider key (and custom URL/name) so you can re-enter fresh values |
135
+ | `/model` | Select a model — auto-fetches the live model list from the provider |
136
+ | `/theme` | Select a theme |
140
137
  | `/session` | Browse saved sessions |
138
+ | `/session-name` | Set session name (arg optional) |
141
139
  | `/settings` | Show configuration |
142
140
  | `/status` | Show agent status |
143
141
  | `/clear` | Clear the conversation (keeps the session) |
144
142
  | `/new` | Start a fresh conversation — rotates to a brand-new session (the old one is kept on disk) |
143
+ | `/reset` | Fresh conversation — old session file deleted from disk |
145
144
  | `/continue` | Continue the current/last session |
145
+ | `/soul` `/memory` `/user` `/agents` | Edit agent files (SOUL/MEMORY/USER/AGENTS.md) |
146
+ | `/skills` | List installed skills |
146
147
  | `/diff` | Show diff viewer |
147
148
  | `/git` | Run `git status` |
148
149
  | `/compact` | Compact context (truncate old messages) |
@@ -160,9 +161,7 @@ export GEMINI_API_KEY=...
160
161
  export OPENROUTER_API_KEY=...
161
162
  ```
162
163
 
163
- Or inside VectorHead: `/provider` → pick provider → paste key (stored masked in `~/.vector/credentials.json`, permissions `0600`).
164
-
165
- Generic variables: `VECTOR_API_KEY`, `VECTOR_API_URL`, `VECTOR_MODEL`, `VECTOR_PROVIDER`, `VECTOR_THEME`, `VECTOR_HOME`, `VECTOR_AUTO_APPROVE` (=1 auto-approves ASK-level permission prompts, for automated runs/tests; dangerous actions still prompt).
164
+ Or inside VectorHead: `/connect` → pick provider → paste key (stored masked in `~/.vector/credentials.json`, permissions `0600`).
166
165
 
167
166
  ### Custom API (OpenAI-compatible)
168
167
 
@@ -174,12 +173,13 @@ vector --provider custom --model deepseek-v3
174
173
 
175
174
  The provider speaks `/v1/chat/completions` (and `/models`). Anthropic and Gemini protocols for custom endpoints are documented as adapters; OpenAI-compatible endpoints are fully supported.
176
175
 
177
- ### Multiple custom providers (OpenCode-style)
176
+ <details>
177
+ <summary><b>Multiple custom providers</b></summary>
178
+
179
+ <br>
178
180
 
179
181
  Any number of OpenAI-compatible providers can be defined side by side in
180
- `~/.vector/config.json` under `providers` — the same shape OpenCode uses in
181
- its `provider` section (minus the `npm` field, since every custom endpoint
182
- here is OpenAI-compatible):
182
+ `~/.vector/config.json` under `providers`:
183
183
 
184
184
  ```jsonc
185
185
  {
@@ -190,7 +190,7 @@ here is OpenAI-compatible):
190
190
  "name": "BitDeer",
191
191
  "options": {
192
192
  "baseURL": "https://api-inference.bitdeer.ai/v1",
193
- "apiKey": "{env:BITDEER_API_KEY}" // or a plain key, or omit and store via /provider
193
+ "apiKey": "{env:BITDEER_API_KEY}" // or a plain key, or omit and store via /connect
194
194
  },
195
195
  "models": {
196
196
  "deepseek-ai/DeepSeek-V4-Flash": { "name": "DeepSeek V4 Flash" }
@@ -219,33 +219,70 @@ here is OpenAI-compatible):
219
219
 
220
220
  - `provider` is the active id — a built-in, the legacy `custom` slot, or any
221
221
  key of `providers`. Keys are **per provider id**: store each under its own
222
- id in `~/.vector/credentials.json` (0600) via `/provider`, inline them in
222
+ id in `~/.vector/credentials.json` (0600) via `/connect`, inline them in
223
223
  `options.apiKey`, or reference an env var with `"{env:NAME}"`.
224
224
  - `options.headers` adds extra HTTP headers to every request of that provider
225
225
  (e.g. a `User-Agent` required by a client-filtering gateway like
226
226
  AgentRouter). Values may be plain strings or `"{env:NAME}"` templates;
227
227
  headers referencing a missing env var are dropped.
228
228
  - `model` is the **default model** for that provider: when the provider
229
- becomes active (`/provider`, `--provider`, or `VECTOR_PROVIDER`), the active
229
+ becomes active (`/connect`, `--provider`, or `VECTOR_PROVIDER`), the active
230
230
  model automatically follows it unless an explicit `--model` / `VECTOR_MODEL`
231
231
  was given. Handy when each provider speaks its own model ids (e.g.
232
232
  AgentRouter uses `deepseek-v4-flash` while BitDeer uses
233
233
  `deepseek-ai/DeepSeek-V4-Flash`).
234
- - `/provider` lists built-ins + every configured custom and offers
234
+ - `/connect` lists built-ins + every configured custom and offers
235
235
  **Add custom provider** (id → name → base URL → key). `/model` then
236
236
  auto-discovers models from the endpoint's `/models` route and falls back
237
237
  to the `models` map above.
238
238
  - The old single-custom setup (`provider: "custom"` + `apiUrl`/`customName`
239
239
  + key under `custom`) keeps working unchanged.
240
240
 
241
- Switch at runtime with `/provider`, or per-run with
241
+ Switch at runtime with `/connect`, or per-run with
242
242
  `VECTOR_PROVIDER=bitdeer vector …` / `--provider bitdeer`.
243
243
 
244
+ </details>
245
+
246
+ ## Skills
247
+
248
+ VectorHead ships a built-in skill system (agentskills.io format —
249
+ YAML frontmatter + markdown body) with **progressive disclosure**: the agent
250
+ sees a one-line catalog first and loads a skill's full body only when needed
251
+ via the `list_skills` / `read_skill` tools.
252
+
253
+ **14 bundled skills:** `verify-node-project`, `safe-git-workflow`,
254
+ `debug-loop`, `fetch-docs`, `secure-secrets`, `systematic-debugging`,
255
+ `test-driven-development`, `codebase-inspection`, `requesting-code-review`,
256
+ `spike`, `simplify-code`, `grounded-citations`, `blocked-page-recovery`,
257
+ `humanize-writing`.
258
+
259
+ Install your own (multi-root resolution):
260
+
261
+ | Root | Scope |
262
+ |---|---|
263
+ | `~/.vector/skills/<name>/SKILL.md` | personal — always active |
264
+ | `<project>/.agents/skills/<name>/SKILL.md` | project — active when running from that project |
265
+ | `~/.agents/skills/<name>/SKILL.md` | personal (standard `npx skills` layout) |
266
+
267
+ Browse in the TUI with `/skills`.
268
+
269
+ ## Agent files
270
+
271
+ Markdown files auto-seeded in `~/.vector/` (never overwritten by upgrades):
272
+
273
+ | File | Purpose |
274
+ |---|---|
275
+ | `SOUL.md` | Agent identity — slot #1 of the system prompt |
276
+ | `MEMORY.md` | Persistent notes the agent keeps across sessions |
277
+ | `USER.md` | What the agent knows about you |
278
+ | `AGENTS.md` | Project rules (also read from the project root, like this repo's own) |
279
+
280
+ Edit via the TUI (`/soul`, `/memory`, `/user`, `/agents`) or any editor. Size is capped per file to keep prompts lean.
281
+
244
282
  ## Themes
245
283
 
246
- VectorHead adopts [OpenCode's theme system](https://opencode.ai/docs/themes/):
247
- select a built-in theme, or drop your own JSON files into
248
- `~/.vector/themes/` (same files OpenCode uses work verbatim).
284
+ VectorHead has its own theme system: pick a built-in theme, or drop your
285
+ own JSON files into `~/.vector/themes/` — no restart, applies on the fly.
249
286
 
250
287
  ```bash
251
288
  vector --theme tokyonight # one-off
@@ -255,10 +292,13 @@ echo '{ "theme": "nord" }' >> ~/.vector/config.json # (merge manually)
255
292
  # or env: export VECTOR_THEME=matrix
256
293
  ```
257
294
 
258
- Built-ins: `vectorhead` (default, the Exact Freebuff dark palette),
259
- `nord`, `tokyonight`, `gruvbox`, `catppuccin`, `one-dark`, `matrix`.
295
+ Built-ins: `vectorhead` (default — the signature orange & gold VectorHead
296
+ palette), `nord`, `tokyonight`, `gruvbox`, `catppuccin`, `one-dark`, `matrix`.
297
+
298
+ <details>
299
+ <summary><b>Custom themes</b></summary>
260
300
 
261
- ### Custom themes
301
+ <br>
262
302
 
263
303
  A theme is a JSON file with an optional `defs` block of reusable colors
264
304
  plus a semantic `theme` block. Color values may be hex (`#88C0D0`), ANSI
@@ -284,14 +324,16 @@ dark/light variants (`{ "dark": "#000", "light": "#fff" }`), or
284
324
  ```
285
325
 
286
326
  Save it as `~/.vector/themes/my-theme.json`, then `/theme` (or
287
- `vector --theme my-theme`) to use it. Semantic keys adopted from OpenCode
288
- and their effect here: `primary` (brand accent), `accent`/`markdownHeading`
327
+ `vector --theme my-theme`) to use it. Semantic keys and their effect:
328
+ `primary` (brand accent), `accent`/`markdownHeading`
289
329
  (headings, prompt), `text`, `textMuted`, `textFaint`, `markdownCode`,
290
330
  `border`/`borderActive`, `error`/`warning`/`success`/`info`, `diffAdded`/
291
331
  `diffRemoved`/`diffContext`, `backgroundPanel`/`backgroundElement`.
292
332
  Unknown keys and unresolvable colors are ignored, so partial themes keep
293
333
  the default palette for anything you don't override.
294
334
 
335
+ </details>
336
+
295
337
  > Hex colors are emitted as truecolor (24-bit) escapes — terminals need
296
338
  > `COLORTERM=truecolor` for full fidelity (most modern terminals). ANSI
297
339
  > palette indexes and `vectorhead` work everywhere.
@@ -331,11 +373,11 @@ Every shell command is classified:
331
373
  src/
332
374
  ├── cli/ argument parsing, keyboard, bootstrap
333
375
  ├── tui/ terminal UI: chat, input, statusbar, diff, components
334
- ├── agent/ agent loop, planner, context, memory, prompts, session
376
+ ├── agent/ agent loop, planner, context, memory, prompts, session, skills
335
377
  ├── providers/ AIProvider interface + OpenAI/Anthropic/Gemini/OpenRouter/Ollama/Custom
336
- ├── tools/ tool registry + filesystem/shell/search/git/web
378
+ ├── tools/ tool registry + filesystem/shell/search/git/web/skills
337
379
  ├── security/ command policy + permission manager
338
- ├── config/ config, credentials, model catalog
380
+ ├── config/ config, credentials, model catalog, free tier
339
381
  └── utils/ terminal (ANSI), logger, paths
340
382
  ```
341
383
 
@@ -361,19 +403,29 @@ npm run build
361
403
 
362
404
  ## Troubleshooting
363
405
 
364
- - **"No API key configured"** — set the relevant `*_API_KEY` env var or run `/provider` in the TUI.
406
+ - **"No API key configured"** — set the relevant `*_API_KEY` env var or run `/connect` in the TUI. On the free tier this never appears — a default free provider is pre-configured.
365
407
  - **"401" / auth failed** — check the key, or for OpenRouter use `OPENROUTER_API_KEY`.
366
- - **Nothing happens on `vector` in Termux** — make sure your terminal is a TTY and TERM is set (e.g. `export TERM=xterm-256color`).
408
+ - **"model not found" on the free tier** — the free catalog rotates; run `/model` to pick from the live list.
409
+ - **"Rate limited (429) / cooling down"** — VectorHead retries automatically with backoff, and the free gateway cools down briefly instead of burning the shared key. Switch to another free model via `/model` and retry.
410
+ - **Nothing happens on `vector` in Termux** — make sure your terminal is a TTY and TERM is set (e.g. `export TERM=xterm-256color`). If glyphs look broken (█ everywhere, torn logo), ASCII safe mode is already default-on; desktop users can opt back in with `VECTOR_ASCII=0`.
367
411
  - **Runs out of iterations** — raise `maxIterations` in `~/.vector/config.json` or refine the request.
368
- - **Rate limited (429)** — VectorHead retries automatically with backoff; wait and retry.
369
412
 
370
413
  ## Roadmap
371
414
 
372
- - Web search tool (provider-backed) behind the `web` abstraction
373
415
  - Interactive diff approval before edits in safe mode
374
416
  - Persistent permission rules per project
375
417
  - Windows support (WSL-focused)
376
418
 
419
+ ## Changelog
420
+
421
+ ### 0.3.15
422
+ - **Thinking cards are segmented per completed agent step.** Reasoning for each step (think → tool → think → tool) now renders as its own `• Thinking` card instead of stacking every step's reasoning into one ever-growing card — stale reasoning no longer appears to "leak" below the live thinking preview during multi-step tasks like web research.
423
+ - Streaming answers keep the guarded `mergeStreamDelta` accumulation across all providers (no more doubled final text on gateways that resend the full text).
424
+
425
+ ### 0.3.14
426
+ - Removed the anonymous `device-id` / `X-Vector-Device` tracking pair entirely — no device identity is generated or sent.
427
+ - Emoji-aware character widths: `/connect` picker borders align on CJK/emoji terminals.
428
+
377
429
  ## License
378
430
 
379
431
  [MIT](LICENSE)
@@ -288,6 +288,46 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
288
288
  // request history stay well-formed even if a provider regresses.
289
289
  result.toolCalls = result.toolCalls.filter((tc) => !!tc && typeof tc.name === 'string' && tc.name !== '');
290
290
  const hasToolCalls = result.toolCalls.length > 0;
291
+ // TOOL PENUTUP (aturan prompt Freebuff-style): suggest_followups,
292
+ // task_completed, render_ui — dipanggil BERSAMA teks jawaban final dalam
293
+ // respons yang sama. Eksekusinya di-sideload DI SINI — tanpa round-trip
294
+ // LLM lagi: dulu tool ini diperlakukan seperti tool biasa → loop memanggil
295
+ // model SEKALI LAGI sekadar menutup giliran (1 panggilan LLM terbuang per
296
+ // giliran) dan TUI menghidupkan kembali status "Thinking" + baris
297
+ // "Working…" DI BAWAH jawaban final yang sudah tampil (laporan user
298
+ // 2026-09-09: "sudah selesai tugas masih working"). Callback
299
+ // onToolCall/onToolResult TETAP ditembak agar kartu followups / ✓ Task
300
+ // Complete / tombol render_ui tetap ter-render; hanya status yang hilang.
301
+ const CLOSING_TOOLS = new Set(['suggest_followups', 'task_completed', 'render_ui']);
302
+ const closerCalls = hasToolCalls
303
+ ? result.toolCalls.filter((tc) => CLOSING_TOOLS.has(tc.name))
304
+ : [];
305
+ if (result.content.trim() && closerCalls.length > 0 && closerCalls.length === result.toolCalls.length) {
306
+ // History tetap well-formed: balasan + toolCalls + hasil tool-nya.
307
+ messages.push({ role: 'assistant', content: result.content, toolCalls: result.toolCalls });
308
+ for (const tc of closerCalls) {
309
+ callbacks.onToolCall?.(tc.name, tc.arguments, tc.id);
310
+ let toolResult;
311
+ try {
312
+ toolResult = await tools.execute(tc.name, tc.arguments, {
313
+ cwd,
314
+ permissions,
315
+ onActivity: callbacks.onActivity,
316
+ maxOutput: 30_000,
317
+ });
318
+ }
319
+ catch (err) {
320
+ toolResult = { output: `ERROR: ${err.message}`, summary: 'tool error' };
321
+ }
322
+ callbacks.onToolResult?.(tc.name, toolResult.output, toolResult.data, tc.id);
323
+ messages.push({ role: 'tool', toolCallId: tc.id, name: tc.name, content: toolResult.output.slice(0, 40_000) });
324
+ }
325
+ finalContent = result.content;
326
+ callbacks.onStatus?.('Completed');
327
+ callbacks.onFinal?.({ ...result, reasoning: lastReasoning || result.reasoning });
328
+ lastReasoning = '';
329
+ return { content: finalContent, iterations, toolCalls: toolCalls + closerCalls.length, stopped, aborted, messages };
330
+ }
291
331
  // Streaming-free path: collect the assistant message and execute tools.
292
332
  messages.push({
293
333
  role: 'assistant',
@@ -80,8 +80,20 @@ function parseFrontmatter(raw) {
80
80
  const yaml = m[1];
81
81
  const body = m[2].trim();
82
82
  const meta = { name: '', description: '' };
83
+ // Multi-line YAML values (folded `>` / literal `|`, e.g. community skills
84
+ // from `npx skills add` use `description: >` with indented continuation
85
+ // lines). Re-join each block into one logical `key: value` line so the
86
+ // simple line-based parser below keeps working unchanged.
87
+ const yamlFlat = yaml.replace(/^([a-zA-Z_-]+):[ \t]*([>|][+-]?\d*)[ \t]*\n((?:[ \t]+.*(?:\n|$))*)/gm, (_all, key, _ind, cont) => {
88
+ const joined = cont
89
+ .split('\n')
90
+ .map((l) => l.trim())
91
+ .filter(Boolean)
92
+ .join(' ');
93
+ return `${key}: ${joined}\n`;
94
+ });
83
95
  const simple = /^([a-zA-Z_-]+):\s*(.+?)\s*$/gm;
84
- for (const match of yaml.matchAll(simple)) {
96
+ for (const match of yamlFlat.matchAll(simple)) {
85
97
  const key = match[1];
86
98
  const value = match[2].replace(/^['"]|['"]$/g, '');
87
99
  if (key === 'name')
@@ -288,6 +288,19 @@ class AnthropicProvider {
288
288
  },
289
289
  };
290
290
  }
291
+ catch (err) {
292
+ // Distinguish a USER interrupt (outer signal aborted → rethrow as-is;
293
+ // the loop marks the run 'Stopped') from OUR 180s deadline. The latter
294
+ // surfaced as a bare AbortError that withRetry refuses to retry — one
295
+ // slow response killed the whole run mid-task ("macet"). Same fix as
296
+ // openai-compat.ts: our deadline = retryable transport failure.
297
+ if (params.signal?.aborted)
298
+ throw err;
299
+ if (err.name === 'AbortError') {
300
+ throw new provider_1.ProviderError('Anthropic stream aborted by timeout (180s).', 0, true);
301
+ }
302
+ throw err;
303
+ }
291
304
  finally {
292
305
  clearTimeout(timer);
293
306
  if (params.signal)
@@ -130,26 +130,45 @@ class GeminiProvider {
130
130
  const alt = stream ? '&alt=sse' : '';
131
131
  return `${BASE}/models/${params.model}:${action}?key=${encodeURIComponent(key)}${alt}`;
132
132
  }
133
- async post(params, stream) {
133
+ post(params, stream, signal) {
134
134
  const controller = new AbortController();
135
135
  const timer = setTimeout(() => controller.abort(), 180_000);
136
136
  const onOuterAbort = () => controller.abort();
137
+ if (signal) {
138
+ if (signal.aborted)
139
+ controller.abort();
140
+ else
141
+ signal.addEventListener('abort', onOuterAbort);
142
+ }
137
143
  if (params.signal) {
138
144
  if (params.signal.aborted)
139
145
  controller.abort();
140
146
  else
141
147
  params.signal.addEventListener('abort', onOuterAbort);
142
148
  }
143
- const res = await fetch(this.url(params, stream), {
149
+ const finish = (res) => {
150
+ clearTimeout(timer);
151
+ if (signal)
152
+ signal.removeEventListener('abort', onOuterAbort);
153
+ if (params.signal)
154
+ params.signal.removeEventListener('abort', onOuterAbort);
155
+ return { res };
156
+ };
157
+ return fetch(this.url(params, stream), {
144
158
  method: 'POST',
145
159
  headers: { 'Content-Type': 'application/json' },
146
160
  body: JSON.stringify(this.buildBody(params, stream)),
147
161
  signal: controller.signal,
162
+ })
163
+ .then(finish)
164
+ .catch((err) => {
165
+ clearTimeout(timer);
166
+ if (signal)
167
+ signal.removeEventListener('abort', onOuterAbort);
168
+ if (params.signal)
169
+ params.signal.removeEventListener('abort', onOuterAbort);
170
+ throw err;
148
171
  });
149
- clearTimeout(timer);
150
- if (params.signal)
151
- params.signal.removeEventListener('abort', onOuterAbort);
152
- return { res };
153
172
  }
154
173
  async chat(params) {
155
174
  const { res } = await this.post(params, false);
@@ -168,11 +187,46 @@ class GeminiProvider {
168
187
  };
169
188
  }
170
189
  async *stream(params) {
171
- const { res } = await this.post(params, true);
172
- if (!res.ok || !res.body) {
173
- const text = await res.text().catch(() => '');
174
- throw new provider_1.ProviderError(`Gemini HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
190
+ // Own abort wiring for the BODY phase: post() clears its 180s timer as
191
+ // soon as headers arrive, so a stream that connects and then STALLS
192
+ // forever had no deadline at all (spinner frozen). Body-phase aborts
193
+ // surface as a bare AbortError that withRetry refuses to retry — one
194
+ // stall killed the run mid-task ("macet"). Our deadline = retryable
195
+ // transport failure; a USER interrupt rethrows untouched.
196
+ const controller = new AbortController();
197
+ const timer = setTimeout(() => controller.abort(), 180_000);
198
+ const onOuterAbort = () => controller.abort();
199
+ if (params.signal) {
200
+ if (params.signal.aborted)
201
+ controller.abort();
202
+ else
203
+ params.signal.addEventListener('abort', onOuterAbort);
204
+ }
205
+ try {
206
+ const posted = await this.post(params, true, controller.signal);
207
+ const res = posted.res;
208
+ if (!res.ok || !res.body) {
209
+ const text = await res.text().catch(() => '');
210
+ throw new provider_1.ProviderError(`Gemini HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
211
+ }
212
+ yield* this.consumeStream(res, params);
175
213
  }
214
+ catch (err) {
215
+ if (params.signal?.aborted)
216
+ throw err;
217
+ if (err.name === 'AbortError') {
218
+ throw new provider_1.ProviderError('Gemini stream stalled or aborted by timeout.', 0, true);
219
+ }
220
+ throw err;
221
+ }
222
+ finally {
223
+ clearTimeout(timer);
224
+ if (params.signal)
225
+ params.signal.removeEventListener('abort', onOuterAbort);
226
+ }
227
+ }
228
+ /** Consume the SSE body of an established streaming response. */
229
+ async *consumeStream(res, params) {
176
230
  const reader = res.body.getReader();
177
231
  const decoder = new TextDecoder();
178
232
  let buffer = '';
@@ -50,26 +50,45 @@ class OllamaProvider {
50
50
  capabilities() {
51
51
  return { streaming: true, tools: true, contextWindow: 128000 };
52
52
  }
53
- async post(body, params) {
53
+ post(body, params, signal) {
54
54
  const controller = new AbortController();
55
55
  const timer = setTimeout(() => controller.abort(), 180_000);
56
56
  const onOuterAbort = () => controller.abort();
57
+ if (signal) {
58
+ if (signal.aborted)
59
+ controller.abort();
60
+ else
61
+ signal.addEventListener('abort', onOuterAbort);
62
+ }
57
63
  if (params.signal) {
58
64
  if (params.signal.aborted)
59
65
  controller.abort();
60
66
  else
61
67
  params.signal.addEventListener('abort', onOuterAbort);
62
68
  }
63
- const res = await fetch(`${baseUrl()}/api/chat`, {
69
+ const finish = (res) => {
70
+ clearTimeout(timer);
71
+ if (signal)
72
+ signal.removeEventListener('abort', onOuterAbort);
73
+ if (params.signal)
74
+ params.signal.removeEventListener('abort', onOuterAbort);
75
+ return { res };
76
+ };
77
+ return fetch(`${baseUrl()}/api/chat`, {
64
78
  method: 'POST',
65
79
  headers: { 'Content-Type': 'application/json' },
66
80
  body: JSON.stringify(body),
67
81
  signal: controller.signal,
82
+ })
83
+ .then(finish)
84
+ .catch((err) => {
85
+ clearTimeout(timer);
86
+ if (signal)
87
+ signal.removeEventListener('abort', onOuterAbort);
88
+ if (params.signal)
89
+ params.signal.removeEventListener('abort', onOuterAbort);
90
+ throw err;
68
91
  });
69
- clearTimeout(timer);
70
- if (params.signal)
71
- params.signal.removeEventListener('abort', onOuterAbort);
72
- return { res };
73
92
  }
74
93
  async chat(params) {
75
94
  const body = {
@@ -113,11 +132,44 @@ class OllamaProvider {
113
132
  };
114
133
  if (params.tools && params.tools.length > 0)
115
134
  body.tools = toToolDefs(params.tools);
116
- const { res } = await this.post(body, params);
117
- if (!res.ok || !res.body) {
118
- const text = await res.text().catch(() => '');
119
- throw new provider_1.ProviderError(`Ollama HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
135
+ // Own abort wiring for the BODY phase: post() clears its 180s timer when
136
+ // headers arrive, so a stream that connects then STALLS forever had no
137
+ // deadline. Same fix as openai-compat: our deadline = retryable
138
+ // transport failure; a USER interrupt rethrows untouched.
139
+ const controller = new AbortController();
140
+ const timer = setTimeout(() => controller.abort(), 180_000);
141
+ const onOuterAbort = () => controller.abort();
142
+ if (params.signal) {
143
+ if (params.signal.aborted)
144
+ controller.abort();
145
+ else
146
+ params.signal.addEventListener('abort', onOuterAbort);
147
+ }
148
+ try {
149
+ const posted = await this.post(body, params, controller.signal);
150
+ const res = posted.res;
151
+ if (!res.ok || !res.body) {
152
+ const text = await res.text().catch(() => '');
153
+ throw new provider_1.ProviderError(`Ollama HTTP ${res.status}: ${text.slice(0, 200)}`, res.status, res.status >= 500);
154
+ }
155
+ yield* this.consumeStream(res, params);
156
+ }
157
+ catch (err) {
158
+ if (params.signal?.aborted)
159
+ throw err;
160
+ if (err.name === 'AbortError') {
161
+ throw new provider_1.ProviderError('Ollama stream stalled or aborted by timeout.', 0, true);
162
+ }
163
+ throw err;
120
164
  }
165
+ finally {
166
+ clearTimeout(timer);
167
+ if (params.signal)
168
+ params.signal.removeEventListener('abort', onOuterAbort);
169
+ }
170
+ }
171
+ /** Consume the NDJSON body of an established streaming response. */
172
+ async *consumeStream(res, params) {
121
173
  const reader = res.body.getReader();
122
174
  const decoder = new TextDecoder();
123
175
  let buffer = '';
@@ -20,8 +20,19 @@ exports.withRetry = withRetry;
20
20
  function mergeStreamDelta(existing, delta) {
21
21
  if (!delta)
22
22
  return existing;
23
- if (existing.endsWith(delta))
24
- return existing; // repeat of the trailing chunk
23
+ if (existing.endsWith(delta)) {
24
+ // Repeat of the trailing chunk → treat as a resend and skip. KNOWN
25
+ // LIMITATION (deliberate, 2026-09-09 audit): a GENUINE next chunk that
26
+ // merely equals the tail (a model repeating a word across a chunk edge)
27
+ // is swallowed too — one repeated token lost. Distinguishing the two
28
+ // without per-connection framing is impossible: a tail resend of a full
29
+ // word ("Hello world" + "world") and a genuine repeat are
30
+ // byte-identical, and any seam heuristic that appends the latter also
31
+ // DOUBLES the former (the exact "dobel" bug this guard exists to
32
+ // prevent, 2026-09-08). Losing one token degrades gracefully; a doubled
33
+ // answer does not. Do not "fix" this without real stream framing.
34
+ return existing;
35
+ }
25
36
  if (existing.startsWith(delta))
26
37
  return existing; // earlier chunk re-emitted
27
38
  if (delta.startsWith(existing))
@@ -75,8 +86,19 @@ function friendlyApiError(err) {
75
86
  503: 'provider unavailable',
76
87
  504: 'timeout',
77
88
  };
78
- if (status !== undefined && labelByStatus[status])
89
+ if (status !== undefined && labelByStatus[status]) {
90
+ // 400 is the ONE status whose body is usually the most actionable
91
+ // detail (strict-gateway protocol errors: "tool_calls without a
92
+ // response", "unknown tool_call_id", context too long). A generic
93
+ // "bad request" label hides exactly what the user needs to fix
94
+ // (agent dying mid-task, "macet", diagnostics). ProviderError already
95
+ // flattened newlines to one line — show the (capped) body instead.
96
+ if (status === 400 && raw && !labelByStatus[status].includes(raw.slice(0, 20))) {
97
+ const text = raw.length > 64 ? `${raw.slice(0, 63)}…` : raw;
98
+ return text;
99
+ }
79
100
  return labelByStatus[status];
101
+ }
80
102
  // Cut at the first `{` — everything from there on is a body dump.
81
103
  const noJson = raw.split('{')[0].trim();
82
104
  // Drop redundant `HTTP <code>:` / `<Name> HTTP <code>:` prefixes — the
@@ -97,6 +119,14 @@ onRetry, } = {}) {
97
119
  return await fn();
98
120
  }
99
121
  catch (err) {
122
+ // A user interrupt must never be retried — regardless of the error
123
+ // SHAPE it arrives in. Providers signal abort either as a DOM
124
+ // AbortError (fetch) or as a plain Error('aborted') (the loop's
125
+ // checkAbort convention); both were previously retryable here via the
126
+ // default-true branch, so Esc during a stall waited 1s/2s/4s before
127
+ // the abort was noticed ("Stopped" felt dead).
128
+ if (signal?.aborted)
129
+ throw err;
100
130
  attempt++;
101
131
  const retryable = err instanceof ProviderError
102
132
  ? err.retryable
package/dist/tui/app.js CHANGED
@@ -166,6 +166,17 @@ class App {
166
166
  * turn's card (dulu: reanchor menarik LAGI batch lama ke dasar transkrip
167
167
  * setiap giliran, jadi kartu followup lama "menempel" di bawah selamanya). */
168
168
  runFollowupMsgs = [];
169
+ /** Tool messages created during the CURRENT run — the boxed live panel
170
+ * above the input renders ONLY these. The old panel scanned the WHOLE
171
+ * transcript backwards with no run boundary, so the moment a new run
172
+ * started (running=true, model still THINKING, no new tool yet) the
173
+ * PREVIOUS run's shell/web-search cards re-appeared above the input —
174
+ * "web-search/shell muncul di area thinking yang bukan tempatnya"
175
+ * (laporan user 2026-09-08, live di VPS: panel penuh `$ Shell find ~ …`
176
+ * dari run lama selagi run baru baru saja berpikir). Cleared per run
177
+ * seperti runFollowupMsgs; kartu tetap tinggal di transkrip sebagai
178
+ * riwayat. */
179
+ runToolMsgs = [];
169
180
  /** Message OBJECT of the last tool line (indexes shift when showWorking
170
181
  * splices the previous working line out — see onToolCall). */
171
182
  lastToolMsg = null;
@@ -701,6 +712,7 @@ class App {
701
712
  this.replyBubble = null;
702
713
  this.wasAbortedByUser = false;
703
714
  this.thinkingMsg = null;
715
+ this.runToolMsgs = [];
704
716
  this.todoMsg = null;
705
717
  this.todoSteps = [];
706
718
  this.todoDone = 0;
@@ -1311,6 +1323,7 @@ class App {
1311
1323
  this.replyHandled = false;
1312
1324
  this.lastToolMsg = null;
1313
1325
  this.thinkingMsg = null;
1326
+ this.runToolMsgs = [];
1314
1327
  // Model-driven todos: the plan's steps become a ☐/☑ checklist right
1315
1328
  // under the user's message, ticking off as tools complete.
1316
1329
  if (planSteps && planSteps.length > 0)
@@ -1404,6 +1417,13 @@ class App {
1404
1417
  this.markDirty();
1405
1418
  this.status = status;
1406
1419
  this.statusColor = (0, theme_1.statusColor)(status);
1420
+ // Tool PENUTUP (suggest_followups/task_completed/render_ui): setelah
1421
+ // jawaban final tampil, status "Running …" TIDAK boleh menghidupkan
1422
+ // kembali baris "Working…" di bawahnya (bug "sudah selesai masih
1423
+ // working", laporan 2026-09-09). Loop kini men-sideload tool-tool ini
1424
+ // tanpa status, tapi pagar tetap terpasang utk provider lama/edge case.
1425
+ if (/^Running (suggest_followups|task_completed|render_ui)/.test(status))
1426
+ return;
1407
1427
  // Just a clean "Working" line — the exact activity detail is shown
1408
1428
  // by the per-tool lines (tool rows) and the bottom status bar, so the
1409
1429
  // working line never reads "Working — Thinking…".
@@ -1468,6 +1488,7 @@ class App {
1468
1488
  shell: { command, isRunning: true },
1469
1489
  };
1470
1490
  this.messages.push(msg);
1491
+ this.runToolMsgs.push(msg);
1471
1492
  this.lastToolMsg = msg;
1472
1493
  this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)}`);
1473
1494
  this.scrollToLatest();
@@ -1507,12 +1528,19 @@ class App {
1507
1528
  },
1508
1529
  };
1509
1530
  this.messages.push(msg);
1531
+ this.runToolMsgs.push(msg);
1510
1532
  this.lastToolMsg = msg;
1511
1533
  this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)}`);
1512
1534
  this.scrollToLatest();
1513
1535
  return;
1514
1536
  }
1515
1537
  }
1538
+ // Tool PENUTUP (datang bersama jawaban final): TIDAK boleh membuat
1539
+ // kartu tool + baris "Working…" baru di bawah jawaban — kartu
1540
+ // followups / ✓ Task Complete / tombol UI-nya sendiri dirender oleh
1541
+ // onToolResult (bug "selesai tapi masih working", laporan 2026-09-09).
1542
+ if (tool === 'suggest_followups' || tool === 'task_completed' || tool === 'render_ui')
1543
+ return;
1516
1544
  let preview = '';
1517
1545
  try {
1518
1546
  const parsed = JSON.parse(args);
@@ -1535,6 +1563,7 @@ class App {
1535
1563
  if (id)
1536
1564
  msg.toolCallId = id;
1537
1565
  this.messages.push(msg);
1566
+ this.runToolMsgs.push(msg);
1538
1567
  this.lastToolMsg = msg;
1539
1568
  this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)}`);
1540
1569
  this.scrollToLatest();
@@ -1607,7 +1636,17 @@ class App {
1607
1636
  this.markDirty();
1608
1637
  return;
1609
1638
  }
1610
- if (this.lastToolMsg?.kind === 'tool') {
1639
+ // Tool PENUTUP (suggest_followups/task_completed/render_ui): output
1640
+ // teksnya TIDAK boleh menempel ke kartu tool LAMA — tool penutup
1641
+ // tidak pernah membuat kartu sendiri (onToolCall early-return)
1642
+ // sehingga `owner` masih menunjuk kartu tool terakhir run biasa;
1643
+ // dulu `[task_completed] …` / `rendered button …` menempel DI BAWAH
1644
+ // OUTPUT KARTU LAMA itu (regresi fix 2026-09-09, kelas bug sama dgn
1645
+ // "web-shell muncul di tempat yang bukan tempatnya", fix eea7151).
1646
+ // Blok render kartu penutup sendiri (group followups / tombol UI)
1647
+ // ada DI BAWAH dan TETAP jalan — hanya preview yang di-skip.
1648
+ const isClosingTool = tool === 'suggest_followups' || tool === 'task_completed' || tool === 'render_ui';
1649
+ if (this.lastToolMsg?.kind === 'tool' && !isClosingTool) {
1611
1650
  const lines = result
1612
1651
  .split('\n')
1613
1652
  .filter((l) => l.trim())
@@ -1677,6 +1716,11 @@ class App {
1677
1716
  this.activeUiButton = w;
1678
1717
  this.messages.push({ kind: 'ui', content: w.text, ui: w, ts: Date.now() });
1679
1718
  }
1719
+ // Tool penutup: kartunya (group followups / tombol UI) sudah
1720
+ // dirender di atas — jangan hidupkan kembali baris "Working… ✓"
1721
+ // setelah jawaban final, dan jangan tick checklist todo plan.
1722
+ if (isClosingTool)
1723
+ return;
1680
1724
  // A finished tool = one more plan step done: tick the next todo.
1681
1725
  this.bumpTodo();
1682
1726
  this.showWorking(`Working — ${(0, chat_1.toolIcon)(tool)} ${(0, chat_1.toolLabel)(tool)} ✓`);
@@ -1825,6 +1869,7 @@ class App {
1825
1869
  this.wasAbortedByUser = false;
1826
1870
  this.running = false;
1827
1871
  this.runStart = null;
1872
+ this.runToolMsgs = [];
1828
1873
  // Re-snap to the REAL bottom: the transient Working bubble made the
1829
1874
  // max scroll several lines larger than the final transcript, so the
1830
1875
  // stored scroll could sit above the true max after the bubble is
@@ -2711,14 +2756,13 @@ class App {
2711
2756
  const n = this.recentToolCount();
2712
2757
  return n > 0 ? n + 2 : 0;
2713
2758
  }
2714
- /** Count of the most recent tool calls (≤ TOOL_PANEL_ROWS). */
2759
+ /** Count of the most recent tool calls (≤ TOOL_PANEL_ROWS). Only tool
2760
+ * lines from the CURRENT run count — the panel must never resurrect the
2761
+ * previous run's shell/web-search cards (bug 2026-09-08: the old whole-
2762
+ * transcript scan re-showed them above the input during the new run's
2763
+ * THINKING phase, before any new tool existed). */
2715
2764
  recentToolCount() {
2716
- let n = 0;
2717
- for (let i = this.messages.length - 1; i >= 0 && n < TOOL_PANEL_ROWS; i--) {
2718
- if (this.messages[i].kind === 'tool')
2719
- n++;
2720
- }
2721
- return n;
2765
+ return Math.min(this.runToolMsgs.length, TOOL_PANEL_ROWS);
2722
2766
  }
2723
2767
  /**
2724
2768
  * The recent-tool-calls panel content: one `· Tool args` line per call
@@ -2731,8 +2775,10 @@ class App {
2731
2775
  return [];
2732
2776
  const out = [];
2733
2777
  const inner = Math.max(8, cols - 4);
2734
- for (let i = this.messages.length - 1; i >= 0 && out.length < TOOL_PANEL_ROWS; i--) {
2735
- const m = this.messages[i];
2778
+ // ONLY this run's tool lines (runToolMsgs), newest last — the panel is
2779
+ // a LIVE indicator of the current work, not a history re-run.
2780
+ for (let i = this.runToolMsgs.length - 1; i >= 0 && out.length < TOOL_PANEL_ROWS; i--) {
2781
+ const m = this.runToolMsgs[i];
2736
2782
  if (m.kind !== 'tool')
2737
2783
  continue;
2738
2784
  const args = (m.content.split('\n')[0] || '').trim();
package/install.sh ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # VectorHead (agen-vektor) — one-shot installer for Debian/Ubuntu & Termux.
4
+ #
5
+ # What it does:
6
+ # 1. Installs Node.js 20+ and npm when missing/too old
7
+ # (Termux: pkg; Debian/Ubuntu: apt, with NodeSource 20.x fallback when
8
+ # the distro ships an older node — e.g. Ubuntu 24.04 ships Node 18).
9
+ # 2. Installs the agent globally: npm i -g agen-vektor
10
+ # 3. Verifies with `vector --version`.
11
+ #
12
+ # Usage:
13
+ # bash install.sh
14
+ #
15
+ set -euo pipefail
16
+
17
+ MIN_MAJOR=20
18
+
19
+ node_major() {
20
+ node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0
21
+ }
22
+
23
+ need_node() {
24
+ command -v node >/dev/null 2>&1 || return 0
25
+ [ "$(node_major)" -lt "$MIN_MAJOR" ]
26
+ }
27
+
28
+ need_npm() {
29
+ ! command -v npm >/dev/null 2>&1
30
+ }
31
+
32
+ SUDO=""
33
+ if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
34
+ SUDO="sudo"
35
+ fi
36
+
37
+ as_root() {
38
+ if [ -n "$SUDO" ]; then
39
+ $SUDO "$@"
40
+ else
41
+ "$@"
42
+ fi
43
+ }
44
+
45
+ say() { printf '\033[1;38;5;208m[vector]\033[0m %s\n' "$*"; }
46
+
47
+ if ! need_node && ! need_npm; then
48
+ say "Node.js $(node -v) & npm sudah terpasang — skip instalasi sistem."
49
+ else
50
+ if [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *com.termux* ]]; then
51
+ say "Termux terdeteksi — pkg update + install nodejs…"
52
+ pkg update -y
53
+ pkg install -y nodejs
54
+ elif command -v apt-get >/dev/null 2>&1; then
55
+ say "apt: update + install nodejs npm…"
56
+ export DEBIAN_FRONTEND=noninteractive
57
+ as_root apt-get update -y
58
+ as_root apt-get install -y curl ca-certificates gnupg nodejs npm || true
59
+ if need_node || need_npm; then
60
+ say "Node distro < ${MIN_MAJOR} atau npm hilang — pakai NodeSource ${MIN_MAJOR}.x…"
61
+ if [ -n "$SUDO" ]; then
62
+ curl -fsSL "https://deb.nodesource.com/setup_${MIN_MAJOR}.x" | $SUDO -E bash -
63
+ else
64
+ curl -fsSL "https://deb.nodesource.com/setup_${MIN_MAJOR}.x" | bash -
65
+ fi
66
+ as_root apt-get install -y nodejs
67
+ fi
68
+ else
69
+ say "Tidak ada apt-get — install Node.js ${MIN_MAJOR}+ manual dari https://nodejs.org lalu jalankan script ini lagi."
70
+ exit 1
71
+ fi
72
+ fi
73
+
74
+ if need_node || need_npm; then
75
+ say "Node/npm masih belum siap (node=$(command -v node >/dev/null && node -v || echo none), npm=$(command -v npm >/dev/null && echo yes || echo no))."
76
+ exit 1
77
+ fi
78
+
79
+ say "Install agen-vektor (global)…"
80
+ npm install -g agen-vektor
81
+
82
+ say "Selesai: $(vector --version 2>/dev/null || echo 'vector') — jalankan dengan perintah: vector"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agen-vektor",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "VectorHead (agen-vektor) — AI Coding Agent CLI/TUI for Linux & Termux. Multi-provider, tool calling, session, permission system.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "main": "dist/cli/index.js",
10
10
  "files": [
11
11
  "dist",
12
+ "install.sh",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],