agen-vektor 0.3.14 → 0.3.15
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 +3 -2
- package/dist/agent/agent.js +10 -49
- package/dist/agent/loop.js +140 -13
- package/dist/agent/prompts.js +11 -1
- package/dist/agent/skills.js +5 -1
- package/dist/cli/index.js +44 -23
- package/dist/cli/keyboard.js +7 -0
- package/dist/config/config.js +36 -3
- package/dist/config/free-tier.js +34 -10
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/gemini.js +1 -1
- package/dist/providers/ollama.js +1 -1
- package/dist/providers/openai-compat.js +41 -4
- package/dist/providers/provider.js +78 -2
- package/dist/tools/design-lab.js +247 -0
- package/dist/tools/extras.js +2 -2
- package/dist/tools/filesystem.js +2 -2
- package/dist/tools/git.js +83 -1
- package/dist/tools/shell.js +1 -1
- package/dist/tools/web.js +44 -3
- package/dist/tui/app.js +263 -141
- package/dist/tui/chat.js +206 -82
- package/dist/tui/commands.js +8 -6
- package/dist/tui/components.js +107 -51
- package/dist/tui/input.js +84 -32
- package/dist/tui/statusbar.js +58 -57
- package/dist/tui/suggest.js +10 -3
- package/dist/utils/terminal.js +161 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ incrementally with no flicker.
|
|
|
57
57
|
|
|
58
58
|
- **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
59
|
- **Agent loop** — plan → inspect → tool call → result → review → fix → retry until done (with an iteration cap).
|
|
60
|
-
- **Tools** — read/write/edit files, list directories, search files, run shell commands, git, fetch web pages.
|
|
60
|
+
- **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).
|
|
61
61
|
- **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
62
|
- **Multi-provider** — OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible custom endpoint.
|
|
63
63
|
- **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.
|
|
@@ -232,7 +232,7 @@ here is OpenAI-compatible):
|
|
|
232
232
|
AgentRouter uses `deepseek-v4-flash` while BitDeer uses
|
|
233
233
|
`deepseek-ai/DeepSeek-V4-Flash`).
|
|
234
234
|
- `/provider` 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`
|
|
@@ -316,6 +316,7 @@ Every shell command is classified:
|
|
|
316
316
|
| Your configured AI provider (OpenAI, Anthropic, Gemini, OpenRouter, Ollama, custom) | Only when you chat — with **your own** API key |
|
|
317
317
|
| `vector-tui.methatech.eu.org` (VectorHead Free gateway) | Only if you pick the free tier; the shared provider key stays server-side, users never see it |
|
|
318
318
|
| Brave / Exa / Serper | Only when you use the web-search tool with your own key |
|
|
319
|
+
| `vector-tui.methatech.eu.org/v1/web-search` | Web search when you have NO search key of your own — the query is relayed by the gateway; your own key, when set, always takes precedence and bypasses the relay |
|
|
319
320
|
| Raw model catalog (public JSON) | To populate the free-tier model list |
|
|
320
321
|
|
|
321
322
|
**Stored locally** (all in `~/.vector/`): `config.json` (preferences, no secrets), `credentials.json` (only keys *you* enter, file mode 0600), sessions, memory, logs. Nothing else is written anywhere.
|
package/dist/agent/agent.js
CHANGED
|
@@ -1,44 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.Agent = void 0;
|
|
37
|
-
/**
|
|
38
|
-
* Agent — high-level facade bundling provider, tools, permissions
|
|
39
|
-
* and the loop into a single object the TUI/CLI can drive.
|
|
40
|
-
*/
|
|
41
|
-
const fs = __importStar(require("node:fs"));
|
|
42
4
|
const factory_1 = require("../providers/factory");
|
|
43
5
|
const registry_1 = require("../tools/registry");
|
|
44
6
|
const filesystem_1 = require("../tools/filesystem");
|
|
@@ -47,10 +9,10 @@ const shell_1 = require("../tools/shell");
|
|
|
47
9
|
const git_1 = require("../tools/git");
|
|
48
10
|
const web_1 = require("../tools/web");
|
|
49
11
|
const extras_1 = require("../tools/extras");
|
|
12
|
+
const design_lab_1 = require("../tools/design-lab");
|
|
50
13
|
const apply_patch_1 = require("../tools/apply-patch");
|
|
51
14
|
const permissions_1 = require("../security/permissions");
|
|
52
15
|
const config_1 = require("../config/config");
|
|
53
|
-
const paths_1 = require("../utils/paths");
|
|
54
16
|
const free_tier_1 = require("../config/free-tier");
|
|
55
17
|
const loop_1 = require("./loop");
|
|
56
18
|
const rules_1 = require("./rules");
|
|
@@ -64,12 +26,12 @@ class Agent {
|
|
|
64
26
|
permissions;
|
|
65
27
|
cwd;
|
|
66
28
|
constructor(opts) {
|
|
67
|
-
// Free tier (vectorhead-free):
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
29
|
+
// Free tier (vectorhead-free): the provider ENTRY is (re)defined here on
|
|
30
|
+
// every run (repair), and FIRST-RUN provider/model selection now happens
|
|
31
|
+
// in loadConfig() itself (2026-09-06) — the old Agent-side seed never ran
|
|
32
|
+
// on the real CLI path because buildAgent() always passes a config, so
|
|
33
|
+
// fresh installs landed on 'openrouter' (401 until /connect). No per-
|
|
34
|
+
// device identity: the gateway requires none (removed 2026-09-05).
|
|
73
35
|
const seeded = (0, config_1.effectiveConfig)(opts.config);
|
|
74
36
|
if ((0, free_tier_1.ensureFreeProvider)(seeded)) {
|
|
75
37
|
try {
|
|
@@ -79,10 +41,6 @@ class Agent {
|
|
|
79
41
|
/* read-only home — the in-memory def still works this session */
|
|
80
42
|
}
|
|
81
43
|
}
|
|
82
|
-
if (firstRun) {
|
|
83
|
-
seeded.provider = free_tier_1.FREE_PROVIDER_ID;
|
|
84
|
-
seeded.model = free_tier_1.FREE_DEFAULT_MODEL;
|
|
85
|
-
}
|
|
86
44
|
this.config = seeded;
|
|
87
45
|
this.cwd = opts.cwd || process.cwd();
|
|
88
46
|
// Seed Hermes-style SOUL.md/MEMORY.md/USER.md/SKILL.md on first run
|
|
@@ -105,6 +63,8 @@ class Agent {
|
|
|
105
63
|
// Freebuff-style extras: glob, read_subtree, task_completed,
|
|
106
64
|
// suggest_followups, render_ui.
|
|
107
65
|
...(0, extras_1.createExtrasTools)(),
|
|
66
|
+
// Design-lab gateway pipeline (Codespace + ui-ux-pro-max): run/status/result.
|
|
67
|
+
...(0, design_lab_1.createDesignLabTools)(),
|
|
108
68
|
// Unified-diff editing (Freebuff/Codebuff apply_patch format).
|
|
109
69
|
(0, apply_patch_1.createApplyPatchTool)(),
|
|
110
70
|
// Hermes-style progressive-disclosure skills (list_skills / read_skill).
|
|
@@ -166,6 +126,7 @@ class Agent {
|
|
|
166
126
|
onFinal: cb.onFinal,
|
|
167
127
|
onError: cb.onError,
|
|
168
128
|
onActivity: cb.onActivity,
|
|
129
|
+
onRetryReset: cb.onRetryReset,
|
|
169
130
|
};
|
|
170
131
|
// Permissions callback must be live-updated as well
|
|
171
132
|
this.permissions.setMode(this.config.permissionMode);
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeHistory = sanitizeHistory;
|
|
3
4
|
exports.runAgentLoop = runAgentLoop;
|
|
4
5
|
exports.compactMessages = compactMessages;
|
|
5
6
|
exports.estimateContextSize = estimateContextSize;
|
|
@@ -18,6 +19,78 @@ const light_1 = require("./light");
|
|
|
18
19
|
const prompts_1 = require("./prompts");
|
|
19
20
|
const rules_1 = require("./rules");
|
|
20
21
|
const skills_1 = require("./skills");
|
|
22
|
+
/**
|
|
23
|
+
* Repair a history window so it stays a VALID OpenAI-style conversation.
|
|
24
|
+
*
|
|
25
|
+
* The TUI continues each turn with `lastMessages.slice(-40)`. That slice can
|
|
26
|
+
* land MID-EXCHANGE: a 'tool' result whose assistant.tool_calls was cut off,
|
|
27
|
+
* or an assistant.tool_calls whose results were cut off. Strict OpenAI-
|
|
28
|
+
* compatible gateways reject the whole request with 400 ("tool_calls without
|
|
29
|
+
* a response" / "unknown tool_call_id"), which the user experiences as the
|
|
30
|
+
* agent dying "mid-task" after a few long turns — even though the current
|
|
31
|
+
* task had nothing wrong with it.
|
|
32
|
+
*
|
|
33
|
+
* Rules enforced here:
|
|
34
|
+
* 1. tool results with no preceding matching tool_calls → dropped (orphans);
|
|
35
|
+
* 2. an assistant.tool_calls interrupted by the next user message (or the
|
|
36
|
+
* end of history) is TRIMMED to the calls that were actually answered
|
|
37
|
+
* (dropped entirely when none were);
|
|
38
|
+
* 3. leading tool results (their assistant was sliced away) → dropped.
|
|
39
|
+
*/
|
|
40
|
+
function sanitizeHistory(history) {
|
|
41
|
+
const msgs = history.filter((m) => m.role !== 'system');
|
|
42
|
+
const out = [];
|
|
43
|
+
// toolCallIds of the MOST RECENT assistant.tool_calls still awaiting results.
|
|
44
|
+
let expected = new Set();
|
|
45
|
+
/** Trim the latest assistant's toolCalls down to the answered subset. */
|
|
46
|
+
const trimPendingAssistant = () => {
|
|
47
|
+
if (expected.size === 0)
|
|
48
|
+
return;
|
|
49
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
50
|
+
const prev = out[i];
|
|
51
|
+
if (prev.role === 'assistant' && prev.toolCalls && prev.toolCalls.length > 0) {
|
|
52
|
+
const answered = prev.toolCalls.filter((tc) => !expected.has(tc.id));
|
|
53
|
+
if (answered.length === prev.toolCalls.length)
|
|
54
|
+
break; // fully answered
|
|
55
|
+
if (answered.length === 0)
|
|
56
|
+
out.splice(i, 1);
|
|
57
|
+
else
|
|
58
|
+
out[i] = { ...prev, toolCalls: answered };
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (prev.role === 'user')
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
expected = new Set();
|
|
65
|
+
};
|
|
66
|
+
for (const m of msgs) {
|
|
67
|
+
if (m.role === 'assistant') {
|
|
68
|
+
if (m.toolCalls && m.toolCalls.length > 0) {
|
|
69
|
+
out.push({ ...m, toolCalls: [...m.toolCalls] });
|
|
70
|
+
expected = new Set(m.toolCalls.map((tc) => tc.id));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out.push(m);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (m.role === 'tool') {
|
|
78
|
+
if (!m.toolCallId || !expected.has(m.toolCallId))
|
|
79
|
+
continue; // orphan result
|
|
80
|
+
out.push(m);
|
|
81
|
+
expected.delete(m.toolCallId);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// user: close out any assistant still awaiting results first.
|
|
85
|
+
trimPendingAssistant();
|
|
86
|
+
out.push(m);
|
|
87
|
+
}
|
|
88
|
+
trimPendingAssistant(); // run died mid tool-execution (history tail)
|
|
89
|
+
let start = 0;
|
|
90
|
+
while (start < out.length && out[start].role === 'tool')
|
|
91
|
+
start++;
|
|
92
|
+
return start > 0 ? out.slice(start) : out;
|
|
93
|
+
}
|
|
21
94
|
async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
22
95
|
const { provider, tools, permissions, config, cwd, signal } = opts;
|
|
23
96
|
const maxIterations = config.maxIterations;
|
|
@@ -25,7 +98,7 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
25
98
|
// On a continuation (history present) the project context is not
|
|
26
99
|
// re-injected: the model already saw it in the first turn, and repeating
|
|
27
100
|
// the folder listing on every turn makes the agent re-explain/echo it.
|
|
28
|
-
const history = (opts.history ?? [])
|
|
101
|
+
const history = sanitizeHistory(opts.history ?? []);
|
|
29
102
|
const isContinuation = history.length > 0;
|
|
30
103
|
// Casual small talk ("hallo", "hi", "apa kabar", "kamu siapa", …) gets a
|
|
31
104
|
// plain, direct answer: no project context, no folder listing, no skills
|
|
@@ -95,7 +168,11 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
95
168
|
let final = null;
|
|
96
169
|
for await (const ev of provider.stream(params)) {
|
|
97
170
|
if (ev.type === 'delta' && ev.delta) {
|
|
98
|
-
|
|
171
|
+
// Guarded merge (not naive +=): gateways that resend the full text
|
|
172
|
+
// so far must not accumulate into a doubled answer ("double chat",
|
|
173
|
+
// reported 2026-09-08 — the TUI guarded its own display but this
|
|
174
|
+
// accumulator fed the no-done fallback path naively).
|
|
175
|
+
content = (0, provider_1.mergeStreamDelta)(content, ev.delta);
|
|
99
176
|
callbacks.onDelta?.(ev.delta);
|
|
100
177
|
}
|
|
101
178
|
else if (ev.type === 'delta' && ev.reasoning) {
|
|
@@ -122,6 +199,13 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
122
199
|
}
|
|
123
200
|
}
|
|
124
201
|
if (final) {
|
|
202
|
+
// Defense in depth: a provider that still accumulates naively (custom
|
|
203
|
+
///mock stream implementations) hands us done.result.content exactly
|
|
204
|
+
// doubled when its LAST delta resent the full text — "X…" + "X…" =
|
|
205
|
+
// content+content (custom/mock stream implementations). That shape is never a genuine reply; trust the
|
|
206
|
+
// guarded stream accumulation instead.
|
|
207
|
+
if (content && final.content === content + content)
|
|
208
|
+
final.content = content;
|
|
125
209
|
if (final.reasoning && !lastReasoning) {
|
|
126
210
|
// Reasoning appeared ONLY in the final result (no deltas were
|
|
127
211
|
// streamed): emit once so the TUI still gets the thinking card.
|
|
@@ -154,7 +238,22 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
154
238
|
callbacks.onStatus?.(iterations === 1 ? 'Thinking' : 'Thinking…');
|
|
155
239
|
let result;
|
|
156
240
|
try {
|
|
157
|
-
result = await (0, provider_1.withRetry)(doCall, {
|
|
241
|
+
result = await (0, provider_1.withRetry)(doCall, {
|
|
242
|
+
retries: config.maxRetries,
|
|
243
|
+
signal,
|
|
244
|
+
onRetry: (attempt, err, waitMs) => {
|
|
245
|
+
// Surface the silent retry loop: without this the user stares at a
|
|
246
|
+
// frozen spinner for 1s+2s+4s while the gateway flakes.
|
|
247
|
+
// friendlyApiError keeps provider bodies (JSON, stack-ish text) out
|
|
248
|
+
// of the status bar — the raw dump leaked internals and pushed the
|
|
249
|
+
// timer/mode labels off the row.
|
|
250
|
+
callbacks.onStatus?.(`Retrying ${attempt}/${config.maxRetries} — ${(0, provider_1.friendlyApiError)(err)}, waiting ${Math.round(waitMs / 1000)}s`);
|
|
251
|
+
// A partially-streamed reply from the FAILED attempt must not leak
|
|
252
|
+
// into the UI or the next attempt's reasoning accumulator.
|
|
253
|
+
lastReasoning = '';
|
|
254
|
+
callbacks.onRetryReset?.();
|
|
255
|
+
},
|
|
256
|
+
});
|
|
158
257
|
}
|
|
159
258
|
catch (err) {
|
|
160
259
|
if (err.name === 'AbortError' || err.message === 'aborted') {
|
|
@@ -166,20 +265,21 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
166
265
|
callbacks.onError?.(new Error('API authentication failed (401). Check your API key.'));
|
|
167
266
|
callbacks.onStatus?.('Auth error');
|
|
168
267
|
stopped = true;
|
|
169
|
-
finalContent =
|
|
268
|
+
finalContent = `! API authentication failed. Run \`vector /provider\` to configure your API key.`;
|
|
170
269
|
break;
|
|
171
270
|
}
|
|
172
271
|
if (err instanceof provider_1.ProviderError && err.status === 429) {
|
|
173
272
|
callbacks.onError?.(new Error('Rate limited (429) by the provider.'));
|
|
174
273
|
callbacks.onStatus?.('Rate limited');
|
|
175
274
|
stopped = true;
|
|
176
|
-
finalContent = '
|
|
275
|
+
finalContent = '! Rate limited by the provider. Wait a moment and try again.';
|
|
177
276
|
break;
|
|
178
277
|
}
|
|
179
278
|
callbacks.onError?.(err);
|
|
180
279
|
callbacks.onStatus?.('Error');
|
|
181
280
|
stopped = true;
|
|
182
|
-
|
|
281
|
+
// Same sanitation as the retry line: no raw HTTP/JSON body in chat.
|
|
282
|
+
finalContent = `! Error: ${(0, provider_1.friendlyApiError)(err)}`;
|
|
183
283
|
break;
|
|
184
284
|
}
|
|
185
285
|
// Defense in depth: a misbehaving gateway can yield a sparse toolCalls
|
|
@@ -211,12 +311,21 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
211
311
|
]);
|
|
212
312
|
const runOne = async (tc) => {
|
|
213
313
|
callbacks.onToolCall?.(tc.name, tc.arguments, tc.id);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
314
|
+
let toolResult;
|
|
315
|
+
try {
|
|
316
|
+
toolResult = await tools.execute(tc.name, tc.arguments, {
|
|
317
|
+
cwd,
|
|
318
|
+
permissions,
|
|
319
|
+
onActivity: callbacks.onActivity,
|
|
320
|
+
maxOutput: 30_000,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
// Freebuff ToolCallItem: a FAILED call still leaves its card with a
|
|
325
|
+
// preview (input-built) but no "running" suffix — surface the error
|
|
326
|
+
// as a failed result so the TUI can finalize the card.
|
|
327
|
+
toolResult = { output: `ERROR: ${err.message}`, summary: 'tool error' };
|
|
328
|
+
}
|
|
220
329
|
return { tc, toolResult };
|
|
221
330
|
};
|
|
222
331
|
try {
|
|
@@ -266,6 +375,24 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
266
375
|
}
|
|
267
376
|
continue;
|
|
268
377
|
}
|
|
378
|
+
// No tool calls → final answer. GUARD: an EMPTY response with no tool
|
|
379
|
+
// calls is a protocol glitch (gateway truncation, overloaded edge, a
|
|
380
|
+
// model that emitted only reasoning) — treating it as the final answer
|
|
381
|
+
// silently ends the run mid-task (reported as "macet di tengah jalan").
|
|
382
|
+
if (!result.content.trim() && result.toolCalls.length === 0) {
|
|
383
|
+
const empty = new provider_1.ProviderError('Upstream returned an empty response (no content, no tool calls).', 0, true);
|
|
384
|
+
if (iterations >= maxIterations) {
|
|
385
|
+
// No budget left for another model call — surface honestly.
|
|
386
|
+
callbacks.onError?.(empty);
|
|
387
|
+
callbacks.onStatus?.('Error');
|
|
388
|
+
stopped = true;
|
|
389
|
+
finalContent = `! ${empty.message}`;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
callbacks.onStatus?.('Empty response — retrying');
|
|
393
|
+
lastReasoning = '';
|
|
394
|
+
continue; // consume the iteration, ask the model again
|
|
395
|
+
}
|
|
269
396
|
// No tool calls → final answer
|
|
270
397
|
finalContent = result.content;
|
|
271
398
|
callbacks.onStatus?.('Completed');
|
|
@@ -276,7 +403,7 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
276
403
|
if (iterations >= maxIterations) {
|
|
277
404
|
callbacks.onStatus?.('Iteration limit');
|
|
278
405
|
if (!finalContent) {
|
|
279
|
-
finalContent =
|
|
406
|
+
finalContent = `! Reached the maximum of ${maxIterations} iterations without completing the task. Run /settings to raise the limit or refine the request.`;
|
|
280
407
|
}
|
|
281
408
|
}
|
|
282
409
|
return { content: finalContent, iterations, toolCalls, stopped, aborted, messages };
|
package/dist/agent/prompts.js
CHANGED
|
@@ -59,7 +59,17 @@ Rules:
|
|
|
59
59
|
write_todos with ["Buat file", "Baca isinya"] before calling write_file.
|
|
60
60
|
The ONLY exceptions — greetings, plain questions, small talk, and
|
|
61
61
|
requests needing just ONE tool call: answer those directly with no
|
|
62
|
-
write_todos
|
|
62
|
+
write_todos.
|
|
63
|
+
14. Suggested followups (Freebuff parity): end EVERY task response by
|
|
64
|
+
calling suggest_followups with exactly 3 clickable followup prompts
|
|
65
|
+
the user is likely to want next. Call it LAST — after the final
|
|
66
|
+
answer text and after all other tool calls. Each followup: a short
|
|
67
|
+
label (2–5 words) and a prompt phrased in the user's voice, one
|
|
68
|
+
sentence naming the OUTCOME (never the steps, file paths, or design
|
|
69
|
+
decisions). Make them specific to what you just did — alternatives,
|
|
70
|
+
related features, testing/verification you could run, or "Continue
|
|
71
|
+
with the next step" when a plan has more steps. Skip it only when
|
|
72
|
+
there is no sensible next step (greetings, goodbyes).`;
|
|
63
73
|
/**
|
|
64
74
|
* Identity line injected into the system prompt so the model can answer
|
|
65
75
|
* "which model/provider are you?" truthfully (it has no other way to know).
|
package/dist/agent/skills.js
CHANGED
|
@@ -286,7 +286,11 @@ metadata:
|
|
|
286
286
|
1. \`git status --short\` and \`git log --oneline -5\` to understand state.
|
|
287
287
|
2. Review changes: \`git diff --stat\`, then \`git diff\` for details.
|
|
288
288
|
3. Stage only relevant files: \`git add <paths>\` (never \`git add -A\` blindly).
|
|
289
|
-
4. Commit with a concise message describing WHY, not just WHAT.
|
|
289
|
+
4. Commit with a concise message describing WHY, not just WHAT. When committing
|
|
290
|
+
on the user's behalf, end the message with the attribution footer from the
|
|
291
|
+
git tool guidance (COMMIT_ATTRIBUTION_FOOTER + COMMIT_ATTRIBUTION_COAUTHOR,
|
|
292
|
+
see \`src/tools/git.ts\`) — unless the commit lands in somebody else's
|
|
293
|
+
repository on their behalf (attribution OFF, no trailer at all).
|
|
290
294
|
5. NEVER run \`git push\` unless the user explicitly asks.
|
|
291
295
|
|
|
292
296
|
## Pitfalls
|
package/dist/cli/index.js
CHANGED
|
@@ -184,7 +184,7 @@ async function runNonInteractive(opts) {
|
|
|
184
184
|
catch {
|
|
185
185
|
preview = args.slice(0, 80);
|
|
186
186
|
}
|
|
187
|
-
console.log(`${terminal_1.ANSI.brightYellow}
|
|
187
|
+
console.log(`${terminal_1.ANSI.brightYellow}· ${t}${terminal_1.ANSI.reset} ${terminal_1.ANSI.dim}${preview}${terminal_1.ANSI.reset}`);
|
|
188
188
|
},
|
|
189
189
|
});
|
|
190
190
|
// Print the final reply. The loop uses the non-streaming path, so the
|
|
@@ -198,7 +198,7 @@ async function runNonInteractive(opts) {
|
|
|
198
198
|
: reply + '\n');
|
|
199
199
|
}
|
|
200
200
|
else if (result.stopped || result.aborted) {
|
|
201
|
-
console.log(`${terminal_1.ANSI.yellow}
|
|
201
|
+
console.log(`${terminal_1.ANSI.yellow}Stopped — no reply was produced.${terminal_1.ANSI.reset}`);
|
|
202
202
|
}
|
|
203
203
|
console.log(`\n${terminal_1.ANSI.dim}— ${result.iterations} iterations, ${result.toolCalls} tool calls${terminal_1.ANSI.reset}`);
|
|
204
204
|
// Persist the conversation (per-project session by default) so the
|
|
@@ -254,12 +254,12 @@ async function runTui(opts) {
|
|
|
254
254
|
// into config via effectiveConfig inside the Agent), then config.json.
|
|
255
255
|
const theme = opts.theme || config.theme || 'vectorhead';
|
|
256
256
|
if (!(0, themes_1.applyThemeByName)(theme)) {
|
|
257
|
-
console.warn(`${terminal_1.ANSI.yellow}
|
|
257
|
+
console.warn(`${terminal_1.ANSI.yellow}!${terminal_1.ANSI.reset} Unknown theme "${theme}" — using vectorhead. Put custom themes in ~/.vector/themes/`);
|
|
258
258
|
(0, themes_1.applyThemeByName)('vectorhead');
|
|
259
259
|
}
|
|
260
260
|
const hasKey = (0, providers_1.providerConfigured)(config, config.provider);
|
|
261
261
|
if (!hasKey) {
|
|
262
|
-
console.log(`${terminal_1.ANSI.yellow}
|
|
262
|
+
console.log(`${terminal_1.ANSI.yellow}!${terminal_1.ANSI.reset} Provider "${(0, factory_1.displayProviderName)(config)}" is not fully configured.\n` +
|
|
263
263
|
`Set ${providerEnv(config.provider)} in your environment, or run /provider inside VectorHead to add one.\n`);
|
|
264
264
|
}
|
|
265
265
|
// Resolve session name — default to a persistent per-project session so
|
|
@@ -317,31 +317,52 @@ async function runTui(opts) {
|
|
|
317
317
|
// frame rows by App.computeFrame, so a selector navigation is diffed per
|
|
318
318
|
// ROW like everything else. The old modal branch forced a full-screen
|
|
319
319
|
// repaint per keystroke (lastFrameRows = []) — the ↑/↓ "berkedip" in
|
|
320
|
-
// /model. The cursor is HIDDEN
|
|
321
|
-
//
|
|
320
|
+
// /model. The hardware cursor is now HIDDEN ALWAYS (Freebuff/opentui
|
|
321
|
+
// parity) — see the note in doRender below.
|
|
322
322
|
let lastFrameRows = [];
|
|
323
|
-
let
|
|
323
|
+
let lastCols = 0;
|
|
324
|
+
let lastRows = 0;
|
|
324
325
|
const doRender = () => {
|
|
325
326
|
const snap = app.frameSnapshot();
|
|
326
327
|
const rows = snap.rows;
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
328
|
+
const { rows: nowRows, cols: nowCols } = (0, terminal_1.getTerminalSize)();
|
|
329
|
+
// Mobile keyboard: when the terminal size CHANGES (keyboard up/down,
|
|
330
|
+
// rotation), Termux's alt-screen can shift/scroll, so incremental row
|
|
331
|
+
// diffs land at wrong offsets — logo/header painted TWICE (report:
|
|
332
|
+
// "makin parah"). Freebuff parity (@opentui full redraw on resize):
|
|
333
|
+
// a size change forces a FULL repaint — clear the whole alt screen and
|
|
334
|
+
// repaint every row from scratch. Between ticks (same size) the cheap
|
|
335
|
+
// row-diff still applies.
|
|
336
|
+
const resized = nowCols !== lastCols || nowRows !== lastRows;
|
|
337
|
+
let out;
|
|
338
|
+
if (resized) {
|
|
339
|
+
// Full repaint: absolute cursorTo per row — NEVER a trailing '\r\n'
|
|
340
|
+
// (an explicit LF on the bottom row SCROLLS the alt screen by one;
|
|
341
|
+
// every later diff would then paint one row off — ghost/double bars).
|
|
342
|
+
out = [terminal_1.ANSI.clearScreen + terminal_1.ANSI.cursorHome];
|
|
343
|
+
for (let i = 0; i < rows.length; i++)
|
|
344
|
+
out.push((0, terminal_1.cursorTo)(i + 1, 1) + rows[i]);
|
|
345
|
+
lastFrameRows = rows;
|
|
337
346
|
}
|
|
338
347
|
else {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
out
|
|
348
|
+
// Keyboard shrink: when the terminal loses rows, the new frame is
|
|
349
|
+
// shorter than the previous screen. Stale rows below the new frame
|
|
350
|
+
// must be CLEARED or the old input-box border + status bar remain
|
|
351
|
+
// painted there — the "double ╭╰ + Build • YOLO" report.
|
|
352
|
+
out = (0, terminal_1.diffFrameUpdates)(lastFrameRows, rows);
|
|
353
|
+
lastFrameRows = rows;
|
|
344
354
|
}
|
|
355
|
+
lastCols = nowCols;
|
|
356
|
+
lastRows = nowRows;
|
|
357
|
+
// Hardware cursor SELALU disembunyikan (Freebuff/opentui parity): kursor
|
|
358
|
+
// terminal asli BERKEDIP dengan cadance bawaan terminal (~500ms). Setelah
|
|
359
|
+
// dot header & sheen logo dihapus, INILAH kedip terakhir yang dilaporkan
|
|
360
|
+
// user (2026-09-07: "masih ada blink di sebelah YOLO dan di dalam chat
|
|
361
|
+
// prompt") — dulu showCursor di-push di akhir tiap paint lalu kursor
|
|
362
|
+
// dibiarkan VISIBLE selama idle → berkedip terus tanpa henti. Caret kini
|
|
363
|
+
// murni blok ▍ statis lukisan InputBox.render (tidak berkedip);
|
|
364
|
+
// showCursor dikembalikan sekali saat exit (blok finally di bawah).
|
|
365
|
+
out.push(terminal_1.ANSI.hideCursor);
|
|
345
366
|
// DECAWM guard: even one exactly-`cols`-wide row (input-box borders,
|
|
346
367
|
// header at the old width) leaves the cursor in the pending-wrap state;
|
|
347
368
|
// the NEXT repaint — e.g. the ~480ms blink dot — then auto-wraps first
|
|
@@ -425,7 +446,7 @@ async function main() {
|
|
|
425
446
|
}
|
|
426
447
|
// Warnings
|
|
427
448
|
if (opts.yolo) {
|
|
428
|
-
console.warn(`${terminal_1.ANSI.yellow}
|
|
449
|
+
console.warn(`${terminal_1.ANSI.yellow}! WARNING: --yolo mode skips permission prompts for ask-level actions.${terminal_1.ANSI.reset}`);
|
|
429
450
|
}
|
|
430
451
|
(0, paths_1.ensureDirs)();
|
|
431
452
|
logger_1.logger.info(`VectorHead v${VERSION} started in ${process.cwd()}`);
|
package/dist/cli/keyboard.js
CHANGED
|
@@ -254,6 +254,13 @@ function parseKey(data) {
|
|
|
254
254
|
i++;
|
|
255
255
|
continue;
|
|
256
256
|
}
|
|
257
|
+
if (code === 20) {
|
|
258
|
+
// Ctrl+T — used by the TUI to cycle the Thinking card (app.ts), so the
|
|
259
|
+
// plain letter 't' can always be typed into the input.
|
|
260
|
+
events.push({ name: 'ctrl_t' });
|
|
261
|
+
i++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
257
264
|
// Printable
|
|
258
265
|
if (code >= 32 && code <= 126) {
|
|
259
266
|
events.push({ name: 'char', char: ch });
|
package/dist/config/config.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DEFAULT_CONFIG = void 0;
|
|
37
|
+
exports.firstRunSeedFree = firstRunSeedFree;
|
|
37
38
|
exports.loadConfig = loadConfig;
|
|
38
39
|
exports.saveConfig = saveConfig;
|
|
39
40
|
exports.effectiveConfig = effectiveConfig;
|
|
@@ -48,9 +49,17 @@ const path = __importStar(require("node:path"));
|
|
|
48
49
|
const paths_1 = require("../utils/paths");
|
|
49
50
|
const logger_1 = require("../utils/logger");
|
|
50
51
|
const providers_1 = require("./providers");
|
|
52
|
+
// Free-tier onboarding (2026-09-06): a FIRST RUN (no config.json) seeds the
|
|
53
|
+
// vectorhead-free provider so `npm install && vector` chats immediately with
|
|
54
|
+
// zero keys (type-only import — no runtime cycle).
|
|
55
|
+
const free_tier_1 = require("./free-tier");
|
|
51
56
|
exports.DEFAULT_CONFIG = {
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
// Free tier is the zero-config default (2026-09-06): the gateway holds the
|
|
58
|
+
// real provider key server-side, so a fresh install needs NO API key.
|
|
59
|
+
// BYOK providers remain one /connect away. Previously this pointed at
|
|
60
|
+
// 'openrouter' (no key → 401 "Not connected" on every fresh install).
|
|
61
|
+
provider: free_tier_1.FREE_PROVIDER_ID,
|
|
62
|
+
model: free_tier_1.FREE_DEFAULT_MODEL,
|
|
54
63
|
protocol: 'openai',
|
|
55
64
|
// User request (2026-09-04): the assistant must ACT without permission
|
|
56
65
|
// prompts — yolo is the default. Dangerous commands (rm, git push, chmod…)
|
|
@@ -65,6 +74,16 @@ exports.DEFAULT_CONFIG = {
|
|
|
65
74
|
compactionThreshold: 80_000,
|
|
66
75
|
theme: 'vectorhead',
|
|
67
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* First-run onboarding seed (2026-09-06): DEFAULT_CONFIG points at the free
|
|
79
|
+
* tier, and the free provider ENTRY is always defined. Returns a NEW object
|
|
80
|
+
* (never mutates DEFAULT_CONFIG) plus true when the caller should persist it.
|
|
81
|
+
*/
|
|
82
|
+
function firstRunSeedFree(base) {
|
|
83
|
+
const config = { ...exports.DEFAULT_CONFIG, ...base };
|
|
84
|
+
const changed = (0, free_tier_1.ensureFreeProvider)(config);
|
|
85
|
+
return { config, changed };
|
|
86
|
+
}
|
|
68
87
|
function loadConfig() {
|
|
69
88
|
(0, paths_1.ensureDirs)();
|
|
70
89
|
const p = (0, paths_1.getConfigPath)();
|
|
@@ -92,9 +111,23 @@ function loadConfig() {
|
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
113
|
catch (err) {
|
|
114
|
+
// UNREADABLE config.json: fall through WITHOUT persisting — never
|
|
115
|
+
// overwrite a user file we failed to parse (their config stays on disk).
|
|
95
116
|
logger_1.logger.warn(`config load failed: ${err.message}`);
|
|
117
|
+
return firstRunSeedFree().config;
|
|
118
|
+
}
|
|
119
|
+
// No config.json (true first run): seed the free tier AND PERSIST it so the
|
|
120
|
+
// next run keeps the same provider (this was the dormant "first-run seeds
|
|
121
|
+
// default to the free tier" promise — the Agent-only seeding never ran on
|
|
122
|
+
// the real CLI path because buildAgent() always passes a config).
|
|
123
|
+
const seeded = firstRunSeedFree();
|
|
124
|
+
try {
|
|
125
|
+
saveConfig(seeded.config);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* read-only home — the in-memory seed still works this session */
|
|
96
129
|
}
|
|
97
|
-
return
|
|
130
|
+
return seeded.config;
|
|
98
131
|
}
|
|
99
132
|
function saveConfig(config) {
|
|
100
133
|
(0, paths_1.ensureDirs)();
|