agen-vektor 0.3.32 → 0.3.34
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 +12 -0
- package/dist/agent/agent.js +11 -0
- package/dist/agent/loop.js +10 -4
- package/dist/agent/prompts.js +7 -1
- package/dist/agent/subagent.js +131 -0
- package/dist/cli/index.js +16 -1
- package/dist/cli/splash.js +167 -0
- package/dist/config/free-tier.js +40 -5
- package/dist/tools/search.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ render cleanly right where you work.*
|
|
|
30
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.
|
|
31
31
|
- **Agent loop** — plan → inspect → tool call → result → review → fix → retry until done (with an iteration cap).
|
|
32
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). Extensible via **MCP servers** (`mcp__<server>__<tool>`, local stdio JSON-RPC, always permission-ask).
|
|
33
|
+
- **Research subagent** — the `task` tool delegates a read-only subtask (map a flow, find all callers of X, survey a module) to a child agent that can only inspect the project and returns a dense factual report — wide exploration costs the main conversation one tool result instead of dozens of read/search calls.
|
|
33
34
|
- **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
35
|
- **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).
|
|
35
36
|
- **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.
|
|
@@ -471,6 +472,17 @@ secrets stay private):
|
|
|
471
472
|
|
|
472
473
|
## Changelog
|
|
473
474
|
|
|
475
|
+
### 0.3.34
|
|
476
|
+
- **Research subagent (`task` tool)** — the agent can now delegate a focused, read-only research subtask ("find every caller of X", "map the auth flow") to a child agent that can only inspect the project (read/list/search/glob/semantic_search — nothing else) and returns a dense factual report. Wide exploration costs the main conversation ONE tool result instead of dozens — the agent stays sharp on big tasks. No recursion, own iteration budget (≤25), report capped, dedicated research prompt.
|
|
477
|
+
- **Default free model = `vector.5.3`** (jalur Atria). The old `z-ai/glm-5.3-free` default now 404s on the gateway; existing installs on a dead id are migrated automatically.
|
|
478
|
+
- **Fix: model choice no longer reverted on launch.** The free-tier startup repair used to overwrite your selected model back to the compiled default every launch — an explicit model that is still servable in the live catalog now survives.
|
|
479
|
+
- **Fix: `search_files` is literal (`-F`).** Queries with regex metacharacters (e.g. `login(`) no longer make ripgrep fail — a text search tool must search text.
|
|
480
|
+
|
|
481
|
+
### 0.3.33
|
|
482
|
+
- **Boot splash "Loading System . . ." before the TUI dashboard.** Launching `vector` now shows a brief boot screen — brand banner, three loading steps (Config → Environment → Provider), a braille spinner and progress bar, then "System ready" — before the whole block is cleanly erased and the chat TUI enters alt-screen. Zero scrollback residue.
|
|
483
|
+
- **Zero dependencies, Termux-safe.** The splash follows `VECTOR_ASCII` (spinner becomes `|/-\` in ASCII safe mode), never blocks process exit (timer is unref'd), and failure to build the agent still clears the splash before the error propagates.
|
|
484
|
+
- **Controls:** `VECTOR_SPLASH=0` disables it entirely; `VECTOR_SPLASH_STEP_MS` (default 450) and `VECTOR_SPLASH_HOLD_MS` (default 900) tune the timing.
|
|
485
|
+
|
|
474
486
|
### 0.3.30
|
|
475
487
|
- **Thinking card matches Freebuff `thinking.tsx` exactly — no mid-run color flip-flop.** The reasoning body renders muted (`#acb3bf`) italic in BOTH streaming and completed states; only the header (dot + bold "Thinking") is foreground-white. Previously the body flipped from white (streaming) to muted (done), which read as the card changing color during a run.
|
|
476
488
|
- **Expanded thinking view is raw muted italic with word wrap.** The in-card markdown re-render was removed — headings/inline code no longer paint their own colors inside the card (the mixed-color expanded view read as noise). Markdown markers in reasoning now show as-is, uniformly styled.
|
package/dist/agent/agent.js
CHANGED
|
@@ -20,6 +20,7 @@ const rules_1 = require("./rules");
|
|
|
20
20
|
const skills_1 = require("./skills");
|
|
21
21
|
const skills_tool_1 = require("../tools/skills-tool");
|
|
22
22
|
const background_1 = require("../tools/background");
|
|
23
|
+
const subagent_1 = require("./subagent");
|
|
23
24
|
const mcp_1 = require("../tools/mcp");
|
|
24
25
|
const logger_1 = require("../utils/logger");
|
|
25
26
|
class Agent {
|
|
@@ -81,6 +82,16 @@ class Agent {
|
|
|
81
82
|
// output_from_background / stop_background_job).
|
|
82
83
|
...(0, background_1.createBackgroundTools)(),
|
|
83
84
|
]);
|
|
85
|
+
// Freebuff-style research subagent: `task` delegates a read-only
|
|
86
|
+
// subtask to a child agent loop (own iteration budget; registry = read
|
|
87
|
+
// tools only; cannot recurse). Registered AFTER the built-ins so its
|
|
88
|
+
// child registry can resolve them.
|
|
89
|
+
this.tools.register((0, subagent_1.createTaskTool)({
|
|
90
|
+
provider: () => this.provider,
|
|
91
|
+
config: () => this.config,
|
|
92
|
+
cwd: this.cwd,
|
|
93
|
+
getTool: (name) => this.tools.get(name),
|
|
94
|
+
}));
|
|
84
95
|
this.initMcpServers();
|
|
85
96
|
}
|
|
86
97
|
async run(request, signal) {
|
package/dist/agent/loop.js
CHANGED
|
@@ -119,10 +119,16 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
119
119
|
const skillsCatalog = (0, skills_1.skillsCatalogText)((0, skills_1.listSkills)(undefined, cwd));
|
|
120
120
|
const messages = [
|
|
121
121
|
// Slot #1: identity (SOUL.md or the built-in VectorHead persona).
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
// Subagent runs override this slot entirely: the child has its own
|
|
123
|
+
// read-only mission prompt and must not inherit the main persona.
|
|
124
|
+
...(opts.systemPrompt
|
|
125
|
+
? [{ role: 'system', content: opts.systemPrompt }]
|
|
126
|
+
: [
|
|
127
|
+
{ role: 'system', content: rulesText || prompts_1.SYSTEM_PROMPT },
|
|
128
|
+
// Core behavior rules (only when SOUL.md is active we still need the
|
|
129
|
+
// operation rules; when rulesText is empty SYSTEM_PROMPT already covers it).
|
|
130
|
+
...(rulesText ? [{ role: 'system', content: prompts_1.SYSTEM_PROMPT }] : []),
|
|
131
|
+
]),
|
|
126
132
|
// Identity: the model must know its own name/provider to answer
|
|
127
133
|
// "kamu model apa / provider mana" truthfully instead of guessing.
|
|
128
134
|
{ role: 'system', content: (0, prompts_1.identityPrompt)(config.model, config.provider) },
|
package/dist/agent/prompts.js
CHANGED
|
@@ -60,7 +60,13 @@ Rules:
|
|
|
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
62
|
write_todos.
|
|
63
|
-
|
|
63
|
+
15. Deep codebase research goes through the 'task' tool: delegate a
|
|
64
|
+
self-contained, read-only subtask ("find every caller of X and how the
|
|
65
|
+
results flow", "map the auth flow", "survey module Y's responsibilities")
|
|
66
|
+
to the research subagent and work from its dense report instead of
|
|
67
|
+
burning many of your own read/search calls. Do NOT use 'task' to edit
|
|
68
|
+
files or run anything mutating.
|
|
69
|
+
16. Suggested followups (Freebuff parity): end EVERY task response by
|
|
64
70
|
calling suggest_followups with exactly 3 clickable followup prompts
|
|
65
71
|
the user is likely to want next. Call it LAST — after the final
|
|
66
72
|
answer text and after all other tool calls. Each followup: a short
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUBAGENT_MAX_ITERATIONS = exports.SUBAGENT_READ_TOOLS = void 0;
|
|
4
|
+
exports.buildSubagentRegistry = buildSubagentRegistry;
|
|
5
|
+
exports.createTaskTool = createTaskTool;
|
|
6
|
+
/**
|
|
7
|
+
* Subagent — Freebuff-style `task` tool: delegates a focused, READ-ONLY
|
|
8
|
+
* research subtask to a child agent loop. The child runs the SAME provider
|
|
9
|
+
* with its own iteration budget and a registry containing ONLY read tools
|
|
10
|
+
* (read_file, list_directory, search_files, glob, read_subtree, list_skills,
|
|
11
|
+
* read_skill, semantic_search) — it physically cannot mutate anything, and
|
|
12
|
+
* `task` itself is not part of its registry so it can never recurse.
|
|
13
|
+
*
|
|
14
|
+
* The child's final answer (a dense factual report) becomes the tool result,
|
|
15
|
+
* so wide codebase exploration costs the MAIN conversation one tool result
|
|
16
|
+
* instead of dozens of read/search tool results.
|
|
17
|
+
*/
|
|
18
|
+
const registry_1 = require("../tools/registry");
|
|
19
|
+
const permissions_1 = require("../security/permissions");
|
|
20
|
+
const loop_1 = require("./loop");
|
|
21
|
+
/** Tools the subagent may use — read-only by construction. */
|
|
22
|
+
exports.SUBAGENT_READ_TOOLS = [
|
|
23
|
+
'read_file',
|
|
24
|
+
'list_directory',
|
|
25
|
+
'search_files',
|
|
26
|
+
'glob',
|
|
27
|
+
'read_subtree',
|
|
28
|
+
'list_skills',
|
|
29
|
+
'read_skill',
|
|
30
|
+
'semantic_search',
|
|
31
|
+
];
|
|
32
|
+
/** Own iteration budget — a research dive must not run forever. */
|
|
33
|
+
exports.SUBAGENT_MAX_ITERATIONS = 25;
|
|
34
|
+
/** Hard cap (chars) on the report returned to the main agent. */
|
|
35
|
+
const REPORT_MAX_CHARS = 12_000;
|
|
36
|
+
const SUBAGENT_SYSTEM_PROMPT = `You are the VectorHead research subagent — a READ-ONLY explorer spawned by the main agent to investigate one subtask inside the current project.
|
|
37
|
+
|
|
38
|
+
MISSION: investigate the subtask and return a DENSE, FACTUAL report.
|
|
39
|
+
|
|
40
|
+
STRICT RULES:
|
|
41
|
+
- You may only INSPECT: read_file, list_directory, search_files, glob, read_subtree, list_skills, read_skill, semantic_search. Mutating tools do not exist for you — never attempt to change anything.
|
|
42
|
+
- Be efficient: locate with search/glob first, then read only the parts that matter. Do not re-read the same file.
|
|
43
|
+
- You have a limited iteration budget — plan, then execute.
|
|
44
|
+
|
|
45
|
+
REPORT FORMAT (your final answer is the ONLY thing the main agent sees):
|
|
46
|
+
- Short bullets of key findings, each anchored with a file path (and line numbers when known).
|
|
47
|
+
- Concrete identifiers (function/class/variable names), call sites and relationships.
|
|
48
|
+
- End with "Bottom line:" plus one paragraph directly answering the subtask.
|
|
49
|
+
- At most ~400 words. No preamble, no pleasantries, do not restate the subtask.`;
|
|
50
|
+
/**
|
|
51
|
+
* Build the child registry: ONLY the read tools present in the parent.
|
|
52
|
+
* `task` itself is deliberately NOT resolvable here (no recursion), and no
|
|
53
|
+
* mutating tool ever enters the child registry.
|
|
54
|
+
*/
|
|
55
|
+
function buildSubagentRegistry(getTool) {
|
|
56
|
+
const registry = new registry_1.ToolRegistry();
|
|
57
|
+
for (const name of exports.SUBAGENT_READ_TOOLS) {
|
|
58
|
+
const tool = getTool(name);
|
|
59
|
+
if (tool)
|
|
60
|
+
registry.register(tool);
|
|
61
|
+
}
|
|
62
|
+
return registry;
|
|
63
|
+
}
|
|
64
|
+
/** One-line preview of raw tool arguments for the activity strip. */
|
|
65
|
+
function previewArgs(raw) {
|
|
66
|
+
const s = raw.replace(/\s+/g, ' ').trim();
|
|
67
|
+
return s.length > 60 ? `${s.slice(0, 57)}…` : s;
|
|
68
|
+
}
|
|
69
|
+
function createTaskTool(deps) {
|
|
70
|
+
return {
|
|
71
|
+
definition: {
|
|
72
|
+
name: 'task',
|
|
73
|
+
description: 'Delegate a focused, READ-ONLY research subtask to a subagent. The subagent can read files, list directories, search, glob and semantic_search — nothing else — and returns a dense factual report (file paths, identifiers, relationships). Use it for wide codebase exploration (map a flow, find all callers of X, survey a module) so your own context stays lean: one task call replaces dozens of read/search calls. Do NOT use it to edit files, run commands, or anything mutating — do those yourself.',
|
|
74
|
+
parameters: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
subtask: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'The specific research question, self-contained (the subagent does not see this conversation).',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ['subtask'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
async execute(args, ctx) {
|
|
86
|
+
const subtask = typeof args.subtask === 'string' ? args.subtask.trim() : '';
|
|
87
|
+
if (!subtask) {
|
|
88
|
+
return { output: 'ERROR: parameter "subtask" (string) is required.' };
|
|
89
|
+
}
|
|
90
|
+
const capped = subtask.slice(0, 2000);
|
|
91
|
+
const tools = buildSubagentRegistry(deps.getTool);
|
|
92
|
+
const config = deps.config();
|
|
93
|
+
const childConfig = {
|
|
94
|
+
...config,
|
|
95
|
+
maxIterations: Math.min(config.maxIterations, exports.SUBAGENT_MAX_ITERATIONS),
|
|
96
|
+
};
|
|
97
|
+
ctx.onActivity?.('task', `subagent: ${previewArgs(capped)}`);
|
|
98
|
+
const result = await (0, loop_1.runAgentLoop)(capped, {
|
|
99
|
+
provider: deps.provider(),
|
|
100
|
+
tools,
|
|
101
|
+
permissions: new permissions_1.PermissionManager({ mode: 'yolo' }),
|
|
102
|
+
config: childConfig,
|
|
103
|
+
cwd: ctx.cwd || deps.cwd,
|
|
104
|
+
signal: ctx.signal,
|
|
105
|
+
systemPrompt: SUBAGENT_SYSTEM_PROMPT,
|
|
106
|
+
}, {
|
|
107
|
+
// Child progress surfaces as parent activity lines — never as chat
|
|
108
|
+
// deltas: the report must land as ONE tool result, and streamed
|
|
109
|
+
// child text must not look like the main agent's answer.
|
|
110
|
+
onStatus: (status) => ctx.onActivity?.('task', status),
|
|
111
|
+
onActivity: (kind, message) => ctx.onActivity?.(kind, message),
|
|
112
|
+
onToolCall: (name, raw) => ctx.onActivity?.(name, previewArgs(raw)),
|
|
113
|
+
});
|
|
114
|
+
let report = (result.content || '').trim();
|
|
115
|
+
if (!report)
|
|
116
|
+
report = '(subagent returned an empty report)';
|
|
117
|
+
if (result.aborted)
|
|
118
|
+
report = `SUBAGENT STOPPED (user abort).\n${report}`;
|
|
119
|
+
else if (report.startsWith('! '))
|
|
120
|
+
report = `SUBAGENT DID NOT FINISH.\n${report}`;
|
|
121
|
+
if (report.length > REPORT_MAX_CHARS) {
|
|
122
|
+
report = `${report.slice(0, REPORT_MAX_CHARS)}\n…(report truncated at ${REPORT_MAX_CHARS} chars)`;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
output: report,
|
|
126
|
+
summary: `subagent: ${result.iterations} iterations, ${result.toolCalls} tool calls`,
|
|
127
|
+
permission: 'safe',
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const factory_1 = require("../providers/factory");
|
|
|
22
22
|
const paths_1 = require("../utils/paths");
|
|
23
23
|
const logger_1 = require("../utils/logger");
|
|
24
24
|
const keyboard_1 = require("./keyboard");
|
|
25
|
+
const splash_1 = require("./splash");
|
|
25
26
|
const terminal_1 = require("../utils/terminal");
|
|
26
27
|
/** Disable auto-wrap margins (DECAWM off): a full-width row then CANNOT
|
|
27
28
|
* push the cursor into the pending-wrap state, so the next write can never
|
|
@@ -248,7 +249,21 @@ async function runTui(opts) {
|
|
|
248
249
|
await runNonInteractive(opts);
|
|
249
250
|
return;
|
|
250
251
|
}
|
|
251
|
-
|
|
252
|
+
// Boot splash "Loading System . . ." (additive, 2026-09-16): tampil
|
|
253
|
+
// sebelum buildAgent/loading dashboard; VECTOR_SPLASH=0 mematikan.
|
|
254
|
+
const splash = await (0, splash_1.runSplash)();
|
|
255
|
+
let agent;
|
|
256
|
+
try {
|
|
257
|
+
await splash.step('config dimuat');
|
|
258
|
+
agent = await buildAgent(opts);
|
|
259
|
+
await splash.step('environment siap');
|
|
260
|
+
await splash.step(`provider ${opts.provider || agent.config.provider}`);
|
|
261
|
+
await splash.finish();
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
await splash.finish();
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
252
267
|
const config = agent.config;
|
|
253
268
|
// OpenCode-style theming: --theme wins, then VECTOR_THEME (already folded
|
|
254
269
|
// into config via effectiveConfig inside the Agent), then config.json.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splashEnabled = splashEnabled;
|
|
4
|
+
exports.runSplash = runSplash;
|
|
5
|
+
/**
|
|
6
|
+
* Boot splash — "Loading System . . ." (2026-09-16).
|
|
7
|
+
*
|
|
8
|
+
* Additive fitur: ditampilkan HANYA di runTui (interactive TTY) sebelum
|
|
9
|
+
* dashboard chat TUI masuk alt-screen. Tidak menyentuh agent, provider,
|
|
10
|
+
* maupun TUI — murni penggambar ANSI di buffer utama lalu DIHAPUS BERSIH
|
|
11
|
+
* sebelum TUI mulai (tidak meninggalkan sampah scrollback).
|
|
12
|
+
*
|
|
13
|
+
* Zero dependency: hanya ANSI escape dari utils/terminal. ASCII safe mode
|
|
14
|
+
* (VECTOR_ASCII=1) otomatis: spinner & simbol di-ganti varian 1 kolom via
|
|
15
|
+
* toAsciiSafe agar tidak merobek layout di Termux/font CJK.
|
|
16
|
+
*
|
|
17
|
+
* Skip: VECTOR_SPLASH=0 mematikan splash sepenuhnya (zero delay) — cocok
|
|
18
|
+
* untuk skrip/otomasi. Step delay bisa diatur VECTOR_SPLASH_STEP_MS.
|
|
19
|
+
*/
|
|
20
|
+
const terminal_1 = require("../utils/terminal");
|
|
21
|
+
/** Spinner frames — braille di mode normal, 1-kolom ASCII di ASCII mode. */
|
|
22
|
+
const SPINNER_BRAILLE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
|
+
const SPINNER_ASCII = ['|', '/', '-', '\\'];
|
|
24
|
+
/** Warna brand (independen dari THEME — theme di-apply SETELAH buildAgent). */
|
|
25
|
+
const ORANGE = '\x1b[38;2;255;133;52m'; // #FF8534 — aksen VectorHead
|
|
26
|
+
const GREEN = '\x1b[38;2;159;252;98m'; // #9EFC62 — sukses VectorHead
|
|
27
|
+
const WHITE = '\x1b[97m';
|
|
28
|
+
const DIM = terminal_1.ANSI.dim;
|
|
29
|
+
const RESET = terminal_1.ANSI.reset;
|
|
30
|
+
/** Jeda per step agar urutan loading terasa (bisa di-override untuk test). */
|
|
31
|
+
const STEP_DELAY_MS = Math.max(0, Number(process.env.VECTOR_SPLASH_STEP_MS ?? 450));
|
|
32
|
+
/** Jeda "System ready" sebelum blok dihapus. */
|
|
33
|
+
const FINISH_HOLD_MS = Math.max(0, Number(process.env.VECTOR_SPLASH_HOLD_MS ?? 900));
|
|
34
|
+
/** Lebar progress bar (kolom). */
|
|
35
|
+
const BAR_WIDTH = 22;
|
|
36
|
+
/** Splash aktif? VECTOR_SPLASH=0 mematikan (env menang, default ON). */
|
|
37
|
+
function splashEnabled() {
|
|
38
|
+
const v = process.env['VECTOR_SPLASH'];
|
|
39
|
+
if (v === undefined)
|
|
40
|
+
return true;
|
|
41
|
+
return v !== '0' && v.toLowerCase() !== 'false';
|
|
42
|
+
}
|
|
43
|
+
/** Panjang terlihat (kolom) dari string ber-ANSI milik splash sendiri. */
|
|
44
|
+
function visibleLen(painted) {
|
|
45
|
+
return painted.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').length;
|
|
46
|
+
}
|
|
47
|
+
/** Bar progress "███░░░░░ 45%" — satu kolom per glyph di kedua mode. */
|
|
48
|
+
function bar(fraction) {
|
|
49
|
+
const pct = Math.max(0, Math.min(1, fraction));
|
|
50
|
+
const filled = Math.round(BAR_WIDTH * pct);
|
|
51
|
+
return (GREEN + '█'.repeat(filled) + DIM + '░'.repeat(BAR_WIDTH - filled) + RESET +
|
|
52
|
+
` ${Math.round(pct * 100)}%`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Jalankan splash: banner sekali + spinner live row. Panggil `step()` per
|
|
56
|
+
* progress, akhiri dengan `finish()` (WAJIB — mengembalikan kursor).
|
|
57
|
+
* Menangani sendiri kasus lebar terminal ekstrim (kolom < 24 → bar disembunyikan).
|
|
58
|
+
*/
|
|
59
|
+
async function runSplash() {
|
|
60
|
+
if (!splashEnabled()) {
|
|
61
|
+
// No-op handle: kode pemanggil tetap bisa memanggil step()/finish().
|
|
62
|
+
return { step: async () => { }, finish: async () => { } };
|
|
63
|
+
}
|
|
64
|
+
const out = process.stdout;
|
|
65
|
+
const cols = Math.max(1, (0, terminal_1.getTerminalSize)().cols);
|
|
66
|
+
const ascii = terminal_1.toAsciiSafe; // ASCII safe mode mengikuti VECTOR_ASCII/Termux
|
|
67
|
+
const frames = asciiModeFrames();
|
|
68
|
+
const showBar = cols >= BAR_WIDTH + 30;
|
|
69
|
+
// Pusatkan satu baris (diukur dari versi TERPAINT agar ASCII map ikut dihitung).
|
|
70
|
+
const center = (text) => {
|
|
71
|
+
const painted = ascii(text);
|
|
72
|
+
const pad = Math.max(0, Math.floor((cols - visibleLen(painted)) / 2));
|
|
73
|
+
return ' '.repeat(pad) + text;
|
|
74
|
+
};
|
|
75
|
+
const lines = []; // jumlah baris yang sudah digambar (untuk clear di finish)
|
|
76
|
+
const write = (s) => {
|
|
77
|
+
out.write(ascii(s));
|
|
78
|
+
};
|
|
79
|
+
const paintLine = (s) => {
|
|
80
|
+
lines.push(s);
|
|
81
|
+
write(center(s) + '\r\n');
|
|
82
|
+
};
|
|
83
|
+
out.write(terminal_1.ANSI.hideCursor);
|
|
84
|
+
// ── Banner (sekali) ──────────────────────────────────────────────
|
|
85
|
+
paintLine(`${ORANGE}▮${RESET} ${WHITE}VectorHead${RESET} ${DIM}v${version()}${RESET}`);
|
|
86
|
+
paintLine(`${DIM}Initializing system components . . .${RESET}`);
|
|
87
|
+
paintLine('');
|
|
88
|
+
// ── Live row (spinner + label + bar) — di-rewrite di tempat ──────
|
|
89
|
+
let frame = 0;
|
|
90
|
+
let stepIndex = 0;
|
|
91
|
+
let label = 'Loading System';
|
|
92
|
+
const renderLive = () => {
|
|
93
|
+
const glyph = frames[frame % frames.length];
|
|
94
|
+
frame++;
|
|
95
|
+
const pct = Math.min(0.92, 0.15 + stepIndex * 0.22);
|
|
96
|
+
const text = `${GREEN}${glyph}${RESET} ${WHITE}${label}${RESET} ${DIM}.${RESET}${DIM}.${RESET}${DIM}.${RESET}` +
|
|
97
|
+
(showBar ? ` ${bar(pct)}` : '');
|
|
98
|
+
// Rewrite in place: kembali ke awal baris live lalu gambar ulang.
|
|
99
|
+
write('\r' + terminal_1.ANSI.clearLineEnd + center(text));
|
|
100
|
+
};
|
|
101
|
+
const liveTimer = setInterval(renderLive, 90);
|
|
102
|
+
// Timer TIDAK boleh menahan proses kalau splash ternyata tidak di-finish
|
|
103
|
+
// (crash path) — biarkan Node berhemi natural.
|
|
104
|
+
if (typeof liveTimer.unref === 'function')
|
|
105
|
+
liveTimer.unref();
|
|
106
|
+
renderLive();
|
|
107
|
+
await sleep(420); // momen awal "Loading System . . ." terlihat jelas
|
|
108
|
+
const names = ['Config', 'Environment', 'Provider'];
|
|
109
|
+
return {
|
|
110
|
+
async step(detail) {
|
|
111
|
+
const name = names[Math.min(stepIndex, names.length - 1)];
|
|
112
|
+
stepIndex++;
|
|
113
|
+
// Ganti live row dengan baris step selesai (append permanen).
|
|
114
|
+
const mark = ascii('✓') === '✓' ? '✓' : 'ok';
|
|
115
|
+
const check = `${GREEN}${mark}${RESET}`;
|
|
116
|
+
const info = detail ? ` ${DIM}${detail}${RESET}` : '';
|
|
117
|
+
write('\r' + terminal_1.ANSI.clearLineEnd); // hapus live row
|
|
118
|
+
paintLine(`${check} ${WHITE}${name}${RESET}${info}`);
|
|
119
|
+
label = `Loading ${names[Math.min(stepIndex, names.length - 1)] ?? 'System'}`;
|
|
120
|
+
renderLive();
|
|
121
|
+
await sleep(STEP_DELAY_MS);
|
|
122
|
+
},
|
|
123
|
+
async finish() {
|
|
124
|
+
clearInterval(liveTimer);
|
|
125
|
+
const mark = ascii('✓') === '✓' ? '✓' : 'ok';
|
|
126
|
+
write('\r' + terminal_1.ANSI.clearLineEnd);
|
|
127
|
+
paintLine(`${GREEN}${mark}${RESET} ${WHITE}System ready${RESET}`);
|
|
128
|
+
write(`\r\n${DIM}Starting VectorHead TUI . . .${RESET}`);
|
|
129
|
+
lines.push('', '');
|
|
130
|
+
await sleep(FINISH_HOLD_MS);
|
|
131
|
+
// Hapus seluruh blok splash → terminal bersih sebelum alt-screen TUI.
|
|
132
|
+
// Kursor berada DI baris terakhir (Starting…) → clearLine dulu, lalu
|
|
133
|
+
// naik + clear per baris tergambar (banner s.d. baris kosong terakhir).
|
|
134
|
+
let clear = terminal_1.ANSI.clearLine;
|
|
135
|
+
for (let i = 0; i < lines.length; i++)
|
|
136
|
+
clear += terminal_1.ANSI.cursorUp(1) + terminal_1.ANSI.clearLine;
|
|
137
|
+
out.write('\r' + clear);
|
|
138
|
+
out.write(terminal_1.ANSI.showCursor);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function asciiModeFrames() {
|
|
143
|
+
// toAsciiSafe memetakan braille → '*' (3 kolom? tidak — catchall 1 kolom),
|
|
144
|
+
// tapi spinner jadi tidak enak dilihat; varian ASCII asli lebih rapi.
|
|
145
|
+
try {
|
|
146
|
+
// Impur dinamis dihindari; cukup cek env yang sama dengan asciiModeEnabled.
|
|
147
|
+
const v = process.env['VECTOR_ASCII'];
|
|
148
|
+
const on = v !== undefined ? v !== '0' && v.toLowerCase() !== 'false' : false;
|
|
149
|
+
return on ? SPINNER_ASCII : SPINNER_BRAILLE;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return SPINNER_BRAILLE;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function sleep(ms) {
|
|
156
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
157
|
+
}
|
|
158
|
+
function version() {
|
|
159
|
+
try {
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
161
|
+
const pkg = require('../../package.json');
|
|
162
|
+
return pkg.version || '0.0.0';
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return '0.0.0';
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/config/free-tier.js
CHANGED
|
@@ -25,6 +25,7 @@ exports.resetFreeCatalogCache = resetFreeCatalogCache;
|
|
|
25
25
|
exports.freeTierEnabled = freeTierEnabled;
|
|
26
26
|
exports.freeProviderDef = freeProviderDef;
|
|
27
27
|
exports.ensureFreeProvider = ensureFreeProvider;
|
|
28
|
+
exports.freeModelServable = freeModelServable;
|
|
28
29
|
exports.isFirstRun = isFirstRun;
|
|
29
30
|
exports.fetchFreeCatalog = fetchFreeCatalog;
|
|
30
31
|
exports.applyFreeCatalog = applyFreeCatalog;
|
|
@@ -66,11 +67,13 @@ exports.FREE_PROVIDER_ID = 'vectorhead-free';
|
|
|
66
67
|
* (ketemu via smoke test 0.3.22). Router Kilo terverifikasi live end-to-end
|
|
67
68
|
* (chat 200, dijawab dots-3-note-preview:free). Riwayat trap yang sama:
|
|
68
69
|
* `deepseek-ai/DeepSeek-V4-Flash` → `claude-sonnet-4-6` → `glm-5.3-flash`
|
|
69
|
-
* (dua flip 2026-09-06/07) → `kilo-auto/free` (2026-09-13) →
|
|
70
|
-
* (2026-09-14
|
|
71
|
-
*
|
|
70
|
+
* (dua flip 2026-09-06/07) → `kilo-auto/free` (2026-09-13) → `z-ai/glm-5.3-free`
|
|
71
|
+
* (2026-09-14) → **vector.5.3** (2026-09-17: gateway live tinggal melayani
|
|
72
|
+
* `glm-5.3-flash-modal` + `vector.5.3` jalur Atria; z-ai/glm-5.3-free 404
|
|
73
|
+
* "model not found" — ketemu live di TUI VPS; vector.5.3 chat 200 terverifikasi
|
|
74
|
+
* live end-to-end di drive subagent hari itu).
|
|
72
75
|
*/
|
|
73
|
-
exports.FREE_DEFAULT_MODEL = '
|
|
76
|
+
exports.FREE_DEFAULT_MODEL = 'vector.5.3';
|
|
74
77
|
/** The providers-map entry for the free tier (idempotent — same shape always). */
|
|
75
78
|
function freeProviderDef() {
|
|
76
79
|
// When a catalog has been fetched in this process, the gateway URL and
|
|
@@ -111,6 +114,7 @@ exports.FREE_LEGACY_MODELS = [
|
|
|
111
114
|
'claude-sonnet-4-6',
|
|
112
115
|
'glm-5.3-flash',
|
|
113
116
|
'kilo-auto/free',
|
|
117
|
+
'z-ai/glm-5.3-free',
|
|
114
118
|
];
|
|
115
119
|
/**
|
|
116
120
|
* Ensure the free provider entry exists in the config's providers map
|
|
@@ -132,6 +136,20 @@ function ensureFreeProvider(config) {
|
|
|
132
136
|
}
|
|
133
137
|
return changed;
|
|
134
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* True when `model` is currently KNOWN-SERVABLE on the free tier: it is the
|
|
141
|
+
* compiled default, OR it appears in the last fetched live catalog (gateway
|
|
142
|
+
* /v1/catalog — includes vector.5.3 via the Atria path). Used to protect a
|
|
143
|
+
* user's explicit model choice from being reverted by the repair step.
|
|
144
|
+
*/
|
|
145
|
+
function freeModelServable(model) {
|
|
146
|
+
if (!model)
|
|
147
|
+
return false;
|
|
148
|
+
if (model === exports.FREE_DEFAULT_MODEL)
|
|
149
|
+
return true;
|
|
150
|
+
const cat = catalogCache?.data;
|
|
151
|
+
return !!cat && Array.isArray(cat.models) && cat.models.some((m) => m && m.id === model);
|
|
152
|
+
}
|
|
135
153
|
/**
|
|
136
154
|
* True when this machine has never run VectorHead before (no config.json).
|
|
137
155
|
* First-run seeds default to the free tier so onboarding is zero-config.
|
|
@@ -185,5 +203,22 @@ async function fetchFreeCatalog(opts) {
|
|
|
185
203
|
*/
|
|
186
204
|
function applyFreeCatalog(data, config) {
|
|
187
205
|
catalogCache = { data, fetchedAt: Date.now() };
|
|
188
|
-
|
|
206
|
+
const changed = ensureFreeProvider(config);
|
|
207
|
+
// Anti-regression (live drive 2026-09-17): an explicit free model that is
|
|
208
|
+
// STILL SERVABLE per the live catalog (e.g. vector.5.3 — owner's choice on
|
|
209
|
+
// the VPS) must survive startup; only a provably-dead id (absent from the
|
|
210
|
+
// catalog, not the default, not legacy) is migrated. Free models are also
|
|
211
|
+
// migrated by ensureFreeProvider above when they appear in
|
|
212
|
+
// FREE_LEGACY_MODELS, so this only catches ids that rotted without ever
|
|
213
|
+
// being listed there. Without this guard a user's model choice could be
|
|
214
|
+
// flipped back to the (possibly stale) compile-time default and 404 forever.
|
|
215
|
+
if (config.provider === exports.FREE_PROVIDER_ID &&
|
|
216
|
+
config.model &&
|
|
217
|
+
config.model !== exports.FREE_DEFAULT_MODEL &&
|
|
218
|
+
!exports.FREE_LEGACY_MODELS.includes(config.model) &&
|
|
219
|
+
!freeModelServable(config.model)) {
|
|
220
|
+
config.model = exports.FREE_DEFAULT_MODEL;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return changed;
|
|
189
224
|
}
|
package/dist/tools/search.js
CHANGED
|
@@ -150,7 +150,13 @@ async function runRg(base, query, filename, max) {
|
|
|
150
150
|
// "--max-count 3" makes ripgrep exit with code 2 ("The argument
|
|
151
151
|
// '--max-count' was provided more than once") — a bug that silently
|
|
152
152
|
// broke every search when rg was installed.
|
|
153
|
-
|
|
153
|
+
// NOTE: -F (fixed strings). The tool description promises a TEXT search,
|
|
154
|
+
// but the query used to be forwarded as a REGEX — a model searching for
|
|
155
|
+
// "login(" (unbalanced paren, live TUI drive 2026-09-17) made rg exit 2
|
|
156
|
+
// "regex parse error" and every such search failed. Models frequently
|
|
157
|
+
// search code fragments with regex metacharacters; literal matching is
|
|
158
|
+
// the least-surprise behavior for a "search text" tool.
|
|
159
|
+
const args = ['--no-ignore', '--line-number', '--max-count', '3', '-F'];
|
|
154
160
|
// "**/<dir>/**" so exclusion works even when the search base is an
|
|
155
161
|
// absolute path outside the rg process cwd (plain "!dir/**" only
|
|
156
162
|
// matches relative to cwd — bit us with tmpdir-based tests).
|
package/package.json
CHANGED