agen-vektor 0.3.11 → 0.3.13
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 +30 -13
- package/dist/agent/agent.js +61 -1
- package/dist/agent/loop.js +5 -0
- package/dist/agent/prompts.js +1 -1
- package/dist/cli/index.js +35 -20
- package/dist/cli/keyboard.js +56 -3
- package/dist/config/config.js +26 -1
- package/dist/config/free-tier.js +230 -0
- package/dist/providers/anthropic.js +34 -1
- package/dist/providers/gemini.js +19 -5
- package/dist/providers/ollama.js +13 -1
- package/dist/providers/openai-compat.js +171 -31
- package/dist/security/command-policy.js +59 -0
- package/dist/security/sensitive-files.js +126 -0
- package/dist/tools/filesystem.js +16 -0
- package/dist/tools/search.js +62 -12
- package/dist/tui/app.js +489 -101
- package/dist/tui/chat.js +119 -43
- package/dist/tui/commands.js +34 -21
- package/dist/tui/components.js +143 -68
- package/dist/tui/input.js +24 -0
- package/dist/tui/statusbar.js +25 -11
- package/dist/tui/suggest.js +27 -9
- package/dist/utils/terminal.js +64 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
```
|
|
2
|
+
████████████████████████████████████████████████
|
|
3
|
+
█ █
|
|
4
|
+
█ V E C T O R A G E N T █
|
|
5
|
+
█ █
|
|
6
|
+
████████████████████████████████████████████████
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
<div align="center">
|
|
2
10
|
|
|
3
11
|
**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.
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
[](https://github.com/clickmamaheti-prog/vector-agent/actions/workflows/ci.yml)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
13
21
|
|
|
14
22
|
```
|
|
15
23
|
VectorHead ● Ready deepseek-ai/DeepSeek-V4-Flash
|
|
@@ -39,6 +47,12 @@ theme-system.ts + markdown-renderer.tsx), a compact agent-activity strip
|
|
|
39
47
|
(⚙ tool calls), and a rounded input box with a block cursor — all rendered
|
|
40
48
|
incrementally with no flicker.
|
|
41
49
|
|
|
50
|
+
## Contents
|
|
51
|
+
|
|
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) · [Architecture](#architecture) · [Development](#development)
|
|
54
|
+
- [Testing](#testing) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [License](#license)
|
|
55
|
+
|
|
42
56
|
## Features
|
|
43
57
|
|
|
44
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.
|
|
@@ -55,10 +69,13 @@ Requirements: **Node.js 20+** and **npm** (Termux: `pkg install nodejs`).
|
|
|
55
69
|
|
|
56
70
|
### From source
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
(membuat `vector` tersedia di PATH).
|
|
72
|
+
Clone this repo, then:
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
```bash
|
|
75
|
+
npm install
|
|
76
|
+
npm run build
|
|
77
|
+
npm link # makes `vector` available on your PATH
|
|
78
|
+
```
|
|
62
79
|
|
|
63
80
|
```bash
|
|
64
81
|
vector
|
|
@@ -80,7 +97,7 @@ npm install -g agen-vektor
|
|
|
80
97
|
vector
|
|
81
98
|
```
|
|
82
99
|
|
|
83
|
-
>
|
|
100
|
+
> Vector Agent has **zero runtime dependencies** and does not assume `systemd`, `sudo`, `apt`, or a desktop GUI — everything runs in a plain terminal.
|
|
84
101
|
|
|
85
102
|
## Quick start
|
|
86
103
|
|
package/dist/agent/agent.js
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
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"));
|
|
4
42
|
const factory_1 = require("../providers/factory");
|
|
5
43
|
const registry_1 = require("../tools/registry");
|
|
6
44
|
const filesystem_1 = require("../tools/filesystem");
|
|
@@ -12,6 +50,8 @@ const extras_1 = require("../tools/extras");
|
|
|
12
50
|
const apply_patch_1 = require("../tools/apply-patch");
|
|
13
51
|
const permissions_1 = require("../security/permissions");
|
|
14
52
|
const config_1 = require("../config/config");
|
|
53
|
+
const paths_1 = require("../utils/paths");
|
|
54
|
+
const free_tier_1 = require("../config/free-tier");
|
|
15
55
|
const loop_1 = require("./loop");
|
|
16
56
|
const rules_1 = require("./rules");
|
|
17
57
|
const skills_1 = require("./skills");
|
|
@@ -24,7 +64,27 @@ class Agent {
|
|
|
24
64
|
permissions;
|
|
25
65
|
cwd;
|
|
26
66
|
constructor(opts) {
|
|
27
|
-
|
|
67
|
+
// Free tier (vectorhead-free): define the provider entry, expose the
|
|
68
|
+
// anonymous device id for the {env:} header template, and default a
|
|
69
|
+
// FIRST RUN (no config.json yet) to the free tier so onboarding is
|
|
70
|
+
// "install & pick a model" with zero keys. Existing configs keep their
|
|
71
|
+
// active provider; only the free ENTRY is (re)defined.
|
|
72
|
+
const firstRun = !opts.config && (0, free_tier_1.isFirstRun)(fs.existsSync((0, paths_1.getConfigPath)()));
|
|
73
|
+
const seeded = (0, config_1.effectiveConfig)(opts.config);
|
|
74
|
+
(0, free_tier_1.exportDeviceIdEnv)();
|
|
75
|
+
if ((0, free_tier_1.ensureFreeProvider)(seeded)) {
|
|
76
|
+
try {
|
|
77
|
+
(0, config_1.saveConfig)(seeded);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* read-only home — the in-memory def still works this session */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (firstRun) {
|
|
84
|
+
seeded.provider = free_tier_1.FREE_PROVIDER_ID;
|
|
85
|
+
seeded.model = free_tier_1.FREE_DEFAULT_MODEL;
|
|
86
|
+
}
|
|
87
|
+
this.config = seeded;
|
|
28
88
|
this.cwd = opts.cwd || process.cwd();
|
|
29
89
|
// Seed Hermes-style SOUL.md/MEMORY.md/USER.md/SKILL.md on first run
|
|
30
90
|
// (never overwrites existing user files — see src/agent/rules.ts).
|
package/dist/agent/loop.js
CHANGED
|
@@ -182,6 +182,11 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
182
182
|
finalContent = `⚠️ Error: ${err.message}`;
|
|
183
183
|
break;
|
|
184
184
|
}
|
|
185
|
+
// Defense in depth: a misbehaving gateway can yield a sparse toolCalls
|
|
186
|
+
// array (null holes — e.g. non-0-based stream indices from Anthropic-
|
|
187
|
+
// backed endpoints). Drop holes here so tool execution and the follow-up
|
|
188
|
+
// request history stay well-formed even if a provider regresses.
|
|
189
|
+
result.toolCalls = result.toolCalls.filter((tc) => !!tc && typeof tc.name === 'string' && tc.name !== '');
|
|
185
190
|
const hasToolCalls = result.toolCalls.length > 0;
|
|
186
191
|
// Streaming-free path: collect the assistant message and execute tools.
|
|
187
192
|
messages.push({
|
package/dist/agent/prompts.js
CHANGED
|
@@ -66,7 +66,7 @@ Rules:
|
|
|
66
66
|
*/
|
|
67
67
|
function identityPrompt(model, provider) {
|
|
68
68
|
return `You are currently running as the model "${model}" via the "${provider}" provider (OpenAI-compatible API unless stated otherwise).
|
|
69
|
-
If the user asks which model you are or which provider backs you, answer with exactly this —
|
|
69
|
+
If the user asks which model you are or which provider backs you, answer with exactly this sentence — copy it verbatim, do not substitute any other name: "Saya dijalankan oleh model ${model} lewat provider ${provider}". Do not invent a different model name.`;
|
|
70
70
|
}
|
|
71
71
|
function taskPrompt(userRequest, cwd) {
|
|
72
72
|
return `Working directory: ${cwd}
|
package/dist/cli/index.js
CHANGED
|
@@ -23,6 +23,12 @@ const paths_1 = require("../utils/paths");
|
|
|
23
23
|
const logger_1 = require("../utils/logger");
|
|
24
24
|
const keyboard_1 = require("./keyboard");
|
|
25
25
|
const terminal_1 = require("../utils/terminal");
|
|
26
|
+
/** Disable auto-wrap margins (DECAWM off): a full-width row then CANNOT
|
|
27
|
+
* push the cursor into the pending-wrap state, so the next write can never
|
|
28
|
+
* flush a phantom newline and scroll the screen (the "dot keluar layar"/
|
|
29
|
+
* jitter bug). Restored after every paint so scrollback stays intact. */
|
|
30
|
+
const DECAWM_OFF = '\x1b[?7l';
|
|
31
|
+
const DECAWM_ON = '\x1b[?7h';
|
|
26
32
|
const themes_1 = require("../tui/themes");
|
|
27
33
|
const session_1 = require("../agent/session");
|
|
28
34
|
const paths_2 = require("../utils/paths");
|
|
@@ -306,23 +312,16 @@ async function runTui(opts) {
|
|
|
306
312
|
// Incremental renderer: keep the previous frame and only rewrite the rows
|
|
307
313
|
// that actually changed. Full-screen redraws every tick caused constant
|
|
308
314
|
// flicker, especially while the agent is running (spinner + timer animate
|
|
309
|
-
// every frame).
|
|
310
|
-
//
|
|
315
|
+
// every frame).
|
|
316
|
+
// OpenCode single-buffer parity: modal overlays are composed INTO the
|
|
317
|
+
// frame rows by App.computeFrame, so a selector navigation is diffed per
|
|
318
|
+
// ROW like everything else. The old modal branch forced a full-screen
|
|
319
|
+
// repaint per keystroke (lastFrameRows = []) — the ↑/↓ "berkedip" in
|
|
320
|
+
// /model. The cursor is HIDDEN while a modal is open (menus don't show a
|
|
321
|
+
// text cursor) and restored at the input row when it closes.
|
|
311
322
|
let lastFrameRows = [];
|
|
312
323
|
let lastCursorPos = '';
|
|
313
324
|
const doRender = () => {
|
|
314
|
-
if (app.modalActive()) {
|
|
315
|
-
// A modal (e.g. a permission prompt) is static while open, so only
|
|
316
|
-
// repaint when something actually changed (open/close/keystroke).
|
|
317
|
-
// Full-screen redraws every tick while waiting for permission caused
|
|
318
|
-
// the chat to flicker during multi-tool turns.
|
|
319
|
-
if (app.isDirty()) {
|
|
320
|
-
lastFrameRows = [];
|
|
321
|
-
process.stdout.write(terminal_1.ANSI.hideCursor + app.render());
|
|
322
|
-
app.consumeRender();
|
|
323
|
-
}
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
325
|
const snap = app.frameSnapshot();
|
|
327
326
|
const rows = snap.rows;
|
|
328
327
|
const out = [];
|
|
@@ -332,12 +331,23 @@ async function runTui(opts) {
|
|
|
332
331
|
}
|
|
333
332
|
}
|
|
334
333
|
lastFrameRows = rows;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
334
|
+
if (app.modalActive()) {
|
|
335
|
+
out.push(terminal_1.ANSI.hideCursor);
|
|
336
|
+
lastCursorPos = '';
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const cursor = (0, terminal_1.cursorTo)(snap.inputRow, snap.cursorCol);
|
|
340
|
+
if (cursor !== lastCursorPos)
|
|
341
|
+
out.push(cursor);
|
|
342
|
+
lastCursorPos = cursor;
|
|
343
|
+
out.push(terminal_1.ANSI.showCursor);
|
|
344
|
+
}
|
|
345
|
+
// DECAWM guard: even one exactly-`cols`-wide row (input-box borders,
|
|
346
|
+
// header at the old width) leaves the cursor in the pending-wrap state;
|
|
347
|
+
// the NEXT repaint — e.g. the ~480ms blink dot — then auto-wraps first
|
|
348
|
+
// and the whole screen scrolls up one line. With auto-wrap disabled for
|
|
349
|
+
// the duration of the paint, that phantom newline is impossible.
|
|
350
|
+
process.stdout.write(terminal_1.ANSI.hideCursor + DECAWM_OFF + (0, terminal_1.toAsciiSafe)(out.filter(Boolean).join('')) + DECAWM_ON);
|
|
341
351
|
};
|
|
342
352
|
const onResize = () => {
|
|
343
353
|
app.markDirty();
|
|
@@ -371,6 +381,10 @@ async function runTui(opts) {
|
|
|
371
381
|
// terminals never send \x1b[<...> sequences, so wheel/touch scrolling of
|
|
372
382
|
// the chat is dead even though the parser supports it.
|
|
373
383
|
process.stdout.write(terminal_1.ANSI.enableMouse);
|
|
384
|
+
// Bracketed paste (Freebuff MultilineInput onPaste): the terminal then
|
|
385
|
+
// wraps clipboard text in ESC[200~ … ESC[201~ so a paste of thousands of
|
|
386
|
+
// characters reaches the input as ONE event instead of a keystroke burst.
|
|
387
|
+
process.stdout.write(terminal_1.ANSI.enablePaste);
|
|
374
388
|
(0, keyboard_1.keyStream)(keyHandler);
|
|
375
389
|
app.start();
|
|
376
390
|
doRender();
|
|
@@ -390,6 +404,7 @@ async function runTui(opts) {
|
|
|
390
404
|
finally {
|
|
391
405
|
process.stdout.write(terminal_1.ANSI.disableAltScreen);
|
|
392
406
|
process.stdout.write(terminal_1.ANSI.disableMouse);
|
|
407
|
+
process.stdout.write(terminal_1.ANSI.disablePaste);
|
|
393
408
|
process.stdout.write(terminal_1.ANSI.showCursor);
|
|
394
409
|
process.stdout.removeListener('resize', onResize);
|
|
395
410
|
cleanup();
|
package/dist/cli/keyboard.js
CHANGED
|
@@ -96,6 +96,22 @@ function parseKey(data) {
|
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
// Bracketed paste: ESC[200~ … ESC[201~ wraps the WHOLE clipboard text
|
|
100
|
+
// (Freebuff onPaste). Captured as ONE 'paste' event so multi-line and
|
|
101
|
+
// multi-thousand-char pastes never burst into per-key events (a raw \n
|
|
102
|
+
// inside the payload would otherwise submit the half-typed prompt).
|
|
103
|
+
if (ch === '\x1b' && s.startsWith('\x1b[200~', i)) {
|
|
104
|
+
const end = s.indexOf('\x1b[201~', i + 6);
|
|
105
|
+
if (end !== -1) {
|
|
106
|
+
events.push({ name: 'paste', text: s.slice(i + 6, end) });
|
|
107
|
+
i = end + 6;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Unterminated paste (cut off read): take the rest as paste text.
|
|
111
|
+
events.push({ name: 'paste', text: s.slice(i + 6) });
|
|
112
|
+
i = s.length;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
99
115
|
// CSI sequence
|
|
100
116
|
if (ch === '\x1b' && i + 1 < s.length && s[i + 1] === '[') {
|
|
101
117
|
// Find the terminating char
|
|
@@ -281,13 +297,50 @@ function enableRawMode() {
|
|
|
281
297
|
process.stdin.pause();
|
|
282
298
|
};
|
|
283
299
|
}
|
|
284
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Create a key stream. Bracketed pastes are BUFFERED across stdin chunks:
|
|
302
|
+
* a multi-thousand-char paste often arrives split over several 'data'
|
|
303
|
+
* events, so the stream waits for the closing ESC[201~ before emitting ONE
|
|
304
|
+
* 'paste' event (Freebuff onPaste). Everything outside a paste is parsed
|
|
305
|
+
* and emitted immediately, exactly like before.
|
|
306
|
+
*/
|
|
285
307
|
function keyStream(onKey) {
|
|
308
|
+
let pending = '';
|
|
286
309
|
process.stdin.on('data', (data) => {
|
|
287
|
-
|
|
288
|
-
|
|
310
|
+
pending += data.toString('utf8');
|
|
311
|
+
for (;;) {
|
|
312
|
+
const start = pending.indexOf('\x1b[200~');
|
|
313
|
+
if (start === -1) {
|
|
314
|
+
const evs = parseKeyStr(pending);
|
|
315
|
+
pending = '';
|
|
316
|
+
for (const ev of evs)
|
|
317
|
+
onKey(ev);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (start > 0) {
|
|
321
|
+
for (const ev of parseKeyStr(pending.slice(0, start)))
|
|
322
|
+
onKey(ev);
|
|
323
|
+
pending = pending.slice(start);
|
|
324
|
+
}
|
|
325
|
+
const end = pending.indexOf('\x1b[201~', 6);
|
|
326
|
+
if (end === -1) {
|
|
327
|
+
// Safety valve: a runaway opener without terminator must not buffer
|
|
328
|
+
// input forever (e.g. a terminal that only half-implements 2004).
|
|
329
|
+
if (pending.length > 1_000_000) {
|
|
330
|
+
onKey({ name: 'paste', text: pending.slice(6) });
|
|
331
|
+
pending = '';
|
|
332
|
+
}
|
|
333
|
+
return; // wait for the rest of the paste
|
|
334
|
+
}
|
|
335
|
+
onKey({ name: 'paste', text: pending.slice(6, end) });
|
|
336
|
+
pending = pending.slice(end + 6);
|
|
337
|
+
}
|
|
289
338
|
});
|
|
290
339
|
}
|
|
340
|
+
/** parseKey over a string (keyStream works on accumulated text). */
|
|
341
|
+
function parseKeyStr(s) {
|
|
342
|
+
return parseKey(Buffer.from(s, 'utf8'));
|
|
343
|
+
}
|
|
291
344
|
function setupReadline(stream) {
|
|
292
345
|
return readline.createInterface({ input: stream, terminal: false });
|
|
293
346
|
}
|
package/dist/config/config.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.effectiveConfig = effectiveConfig;
|
|
|
44
44
|
* credentials.json (0600) or environment variables, never in config.json.
|
|
45
45
|
*/
|
|
46
46
|
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
47
48
|
const paths_1 = require("../utils/paths");
|
|
48
49
|
const logger_1 = require("../utils/logger");
|
|
49
50
|
const providers_1 = require("./providers");
|
|
@@ -51,7 +52,12 @@ exports.DEFAULT_CONFIG = {
|
|
|
51
52
|
provider: 'openrouter',
|
|
52
53
|
model: 'deepseek/deepseek-chat',
|
|
53
54
|
protocol: 'openai',
|
|
54
|
-
|
|
55
|
+
// User request (2026-09-04): the assistant must ACT without permission
|
|
56
|
+
// prompts — yolo is the default. Dangerous commands (rm, git push, chmod…)
|
|
57
|
+
// still prompt and blocked ones are always refused (see command-policy),
|
|
58
|
+
// so the guardrail floor stays intact. Opt back into prompting via
|
|
59
|
+
// VECTOR_ASK=1 or the /mode toggle in the TUI.
|
|
60
|
+
permissionMode: 'yolo',
|
|
55
61
|
maxIterations: 25,
|
|
56
62
|
maxRetries: 3,
|
|
57
63
|
requestTimeoutMs: 120_000,
|
|
@@ -66,6 +72,22 @@ function loadConfig() {
|
|
|
66
72
|
if (fs.existsSync(p)) {
|
|
67
73
|
const raw = fs.readFileSync(p, 'utf8');
|
|
68
74
|
const parsed = JSON.parse(raw);
|
|
75
|
+
// One-time mode migration (2026-09-04): permissionMode became 'yolo'
|
|
76
|
+
// by default — the assistant must act without prompting. Configs that
|
|
77
|
+
// still carry the OLD default ('ask') are flipped once; an explicit
|
|
78
|
+
// /mode toggle afterwards re-persists and is then respected forever.
|
|
79
|
+
if (parsed.permissionMode === 'ask') {
|
|
80
|
+
const marker = path.join((0, paths_1.getVectorDir)(), '.mode-migrated');
|
|
81
|
+
if (!fs.existsSync(marker)) {
|
|
82
|
+
parsed.permissionMode = 'yolo';
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(marker, new Date().toISOString() + '\n');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* best-effort */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
69
91
|
return { ...exports.DEFAULT_CONFIG, ...parsed };
|
|
70
92
|
}
|
|
71
93
|
}
|
|
@@ -92,6 +114,9 @@ function effectiveConfig(base) {
|
|
|
92
114
|
out.theme = process.env.VECTOR_THEME;
|
|
93
115
|
if (process.env.VECTOR_YOLO === '1')
|
|
94
116
|
out.permissionMode = 'yolo';
|
|
117
|
+
// Opt back into per-action confirmation prompts.
|
|
118
|
+
if (process.env.VECTOR_ASK === '1')
|
|
119
|
+
out.permissionMode = 'ask';
|
|
95
120
|
// Per-provider default model: when the provider comes from the environment
|
|
96
121
|
// and no explicit model env was given, follow the provider definition's
|
|
97
122
|
// declared default (config.json providers.<id>.model).
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Free-tier provider (`vectorhead-free`) — the "install & just pick a model"
|
|
4
|
+
* onboarding tier backed by the VectorHead gateway (Cloudflare Worker):
|
|
5
|
+
*
|
|
6
|
+
* https://vector-tui.methatech.eu.org (see konsep.md and vector-gateway/
|
|
7
|
+
* — the worker holds the real BitDeer tokens as server-side secrets and
|
|
8
|
+
* enforces the per-device quota; users never see an API key).
|
|
9
|
+
*
|
|
10
|
+
* Design:
|
|
11
|
+
* - `vectorhead-free` is an OpenCode-style entry in config.providers with an
|
|
12
|
+
* INLINE placeholder key. The gateway identifies users by the anonymous
|
|
13
|
+
* device id sent via the X-Vector-Device header, so no real key exists —
|
|
14
|
+
* the inline placeholder simply satisfies "custom providers need a key"
|
|
15
|
+
* checks without storing anything secret on the user's disk.
|
|
16
|
+
* - The device id lives in ~/.vector/device-id (NOT config.json, so config
|
|
17
|
+
* stays copyable between machines; a copied device id would merge two
|
|
18
|
+
* users into one quota bucket). Header templating "{env:VECTOR_DEVICE_ID}"
|
|
19
|
+
* resolves at request time via providerHeaders().
|
|
20
|
+
* - First run (no config.json): the agent starts on this provider so a new
|
|
21
|
+
* user can chat immediately — pick BYOK later via /connect. An EXISTING
|
|
22
|
+
* config is never touched except to (re)define the free provider entry.
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.FREE_DEFAULT_MODEL = exports.FREE_PROVIDER_ID = exports.CATALOG_TTL_MS = exports.FREE_CATALOG_URL = exports.FREE_GATEWAY_CATALOG_URL = exports.FREE_GATEWAY_URL = void 0;
|
|
59
|
+
exports.resetFreeCatalogCache = resetFreeCatalogCache;
|
|
60
|
+
exports.freeTierEnabled = freeTierEnabled;
|
|
61
|
+
exports.ensureDeviceId = ensureDeviceId;
|
|
62
|
+
exports.exportDeviceIdEnv = exportDeviceIdEnv;
|
|
63
|
+
exports.freeProviderDef = freeProviderDef;
|
|
64
|
+
exports.ensureFreeProvider = ensureFreeProvider;
|
|
65
|
+
exports.isFirstRun = isFirstRun;
|
|
66
|
+
exports.fetchFreeCatalog = fetchFreeCatalog;
|
|
67
|
+
exports.applyFreeCatalog = applyFreeCatalog;
|
|
68
|
+
const fs = __importStar(require("node:fs"));
|
|
69
|
+
const crypto = __importStar(require("node:crypto"));
|
|
70
|
+
const paths_1 = require("../utils/paths");
|
|
71
|
+
/** Public gateway base URL (custom Cloudflare domain — stable, no ports). */
|
|
72
|
+
exports.FREE_GATEWAY_URL = 'https://vector-tui.methatech.eu.org/v1';
|
|
73
|
+
/**
|
|
74
|
+
* Live catalog served BY the gateway itself (admin-editable via the /admin
|
|
75
|
+
* dashboard — upstream, model list and limits can change without touching
|
|
76
|
+
* this repo). Kept in sync with FREE_GATEWAY_URL's /v1 root.
|
|
77
|
+
*/
|
|
78
|
+
exports.FREE_GATEWAY_CATALOG_URL = exports.FREE_GATEWAY_URL.replace(/\/v1$/, '') + '/v1/catalog';
|
|
79
|
+
/**
|
|
80
|
+
* Public catalog (konsep.md tahap 2): JSON WITHOUT secrets committed to the
|
|
81
|
+
* repo and fetched via the stable RAW URL — models, gateway URL, limits and
|
|
82
|
+
* the remote kill switch, all updatable without an app release.
|
|
83
|
+
* FALLBACK only: the gateway's own /v1/catalog (admin-editable) is tried
|
|
84
|
+
* first; this static file covers the gateway being down / not yet deployed.
|
|
85
|
+
*/
|
|
86
|
+
exports.FREE_CATALOG_URL = 'https://raw.githubusercontent.com/clickmamaheti-prog/vector-agent/main/free-models.json';
|
|
87
|
+
/** Catalog cache lifetime (konsep.md: "Agent fetch saat start, cache 1 jam"). */
|
|
88
|
+
exports.CATALOG_TTL_MS = 60 * 60 * 1000;
|
|
89
|
+
/** In-process catalog cache — once loaded, freeProviderDef() follows it. */
|
|
90
|
+
let catalogCache = null;
|
|
91
|
+
/** Test/edge hook: forget the cached catalog (next fetch starts cold). */
|
|
92
|
+
function resetFreeCatalogCache() {
|
|
93
|
+
catalogCache = null;
|
|
94
|
+
}
|
|
95
|
+
/** Remote kill switch ("enabled": false in free-models.json) — default true. */
|
|
96
|
+
function freeTierEnabled() {
|
|
97
|
+
return catalogCache?.data.enabled !== false;
|
|
98
|
+
}
|
|
99
|
+
/** Provider id used in config.provider / providers map. */
|
|
100
|
+
exports.FREE_PROVIDER_ID = 'vectorhead-free';
|
|
101
|
+
/** Default free model (cheap, gateway allowlist-friendly). */
|
|
102
|
+
exports.FREE_DEFAULT_MODEL = 'deepseek-ai/DeepSeek-V4-Flash';
|
|
103
|
+
/** File holding the anonymous per-device id. */
|
|
104
|
+
function devicePath() {
|
|
105
|
+
return process.env.VECTOR_DEVICE_FILE || ((0, paths_1.getVectorDir)() + '/device-id');
|
|
106
|
+
}
|
|
107
|
+
/** Device id for the anonymous free tier: create on first use, reuse after. */
|
|
108
|
+
function ensureDeviceId() {
|
|
109
|
+
try {
|
|
110
|
+
const p = devicePath();
|
|
111
|
+
if (fs.existsSync(p)) {
|
|
112
|
+
const existing = fs.readFileSync(p, 'utf8').trim();
|
|
113
|
+
if (/^[a-f0-9][a-f0-9-]{7,63}$/.test(existing))
|
|
114
|
+
return existing;
|
|
115
|
+
}
|
|
116
|
+
const fresh = crypto.randomUUID();
|
|
117
|
+
fs.writeFileSync(p, fresh + '\n', { mode: 0o600 });
|
|
118
|
+
return fresh;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Read-only fs etc. — fall back to an ephemeral id for this session.
|
|
122
|
+
return crypto.randomUUID();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Export the current device id into the env for {env:} header templates. */
|
|
126
|
+
function exportDeviceIdEnv() {
|
|
127
|
+
try {
|
|
128
|
+
process.env.VECTOR_DEVICE_ID = ensureDeviceId();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* never block startup on this */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** The providers-map entry for the free tier (idempotent — same shape always). */
|
|
135
|
+
function freeProviderDef() {
|
|
136
|
+
// When a catalog has been fetched in this process, the gateway URL and
|
|
137
|
+
// model list follow it (konsep.md: katalog bisa diubah remote tanpa rilis
|
|
138
|
+
// app). No cache → the compile-time defaults.
|
|
139
|
+
const cat = catalogCache?.data;
|
|
140
|
+
const models = {};
|
|
141
|
+
if (cat && Array.isArray(cat.models) && cat.models.length > 0) {
|
|
142
|
+
for (const m of cat.models) {
|
|
143
|
+
if (m && typeof m.id === 'string' && m.id.trim() !== '') {
|
|
144
|
+
models[m.id] = { name: m.label || m.id };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
name: 'VectorHead Free',
|
|
150
|
+
options: {
|
|
151
|
+
baseURL: (cat && typeof cat.gateway === 'string' && cat.gateway ? cat.gateway : exports.FREE_GATEWAY_URL),
|
|
152
|
+
// Placeholder ONLY — the gateway keys users by X-Vector-Device, not
|
|
153
|
+
// by a secret. Inline (not {env:}) so no env var is ever required.
|
|
154
|
+
apiKey: 'vectorhead-free-anonymous',
|
|
155
|
+
headers: { 'X-Vector-Device': '{env:VECTOR_DEVICE_ID}' },
|
|
156
|
+
},
|
|
157
|
+
model: exports.FREE_DEFAULT_MODEL,
|
|
158
|
+
models: Object.keys(models).length > 0 ? models : {
|
|
159
|
+
[exports.FREE_DEFAULT_MODEL]: { name: 'DeepSeek V4 Flash (free)' },
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Ensure the free provider entry exists in the config's providers map
|
|
165
|
+
* (create or repair — never touches unrelated providers). Returns true when
|
|
166
|
+
* the config object was modified.
|
|
167
|
+
*/
|
|
168
|
+
function ensureFreeProvider(config) {
|
|
169
|
+
const cur = config.providers?.[exports.FREE_PROVIDER_ID];
|
|
170
|
+
const want = JSON.stringify(freeProviderDef());
|
|
171
|
+
if (cur && JSON.stringify(cur) === want)
|
|
172
|
+
return false;
|
|
173
|
+
config.providers = { ...(config.providers || {}), [exports.FREE_PROVIDER_ID]: freeProviderDef() };
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* True when this machine has never run VectorHead before (no config.json).
|
|
178
|
+
* First-run seeds default to the free tier so onboarding is zero-config.
|
|
179
|
+
*/
|
|
180
|
+
function isFirstRun(configDirHasConfig) {
|
|
181
|
+
return !configDirHasConfig;
|
|
182
|
+
}
|
|
183
|
+
function isValidCatalog(data) {
|
|
184
|
+
if (!data || typeof data !== 'object')
|
|
185
|
+
return false;
|
|
186
|
+
const c = data;
|
|
187
|
+
return typeof c.gateway === 'string' && /^https:\/\//.test(c.gateway)
|
|
188
|
+
&& Array.isArray(c.models);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Fetch the catalog. Sources, in order:
|
|
192
|
+
* 1. explicit override (opts.url or VECTOR_FREE_CATALOG_URL env) — ONLY source
|
|
193
|
+
* 2. gateway /v1/catalog (live, admin-editable via the /admin dashboard)
|
|
194
|
+
* 3. GitHub RAW free-models.json (static fallback when the gateway is down)
|
|
195
|
+
* Cached for 1 hour — subsequent calls within the TTL are served from memory
|
|
196
|
+
* without network. Returns null when every source fails / is malformed (never
|
|
197
|
+
* throws) so callers can stay best-effort.
|
|
198
|
+
*/
|
|
199
|
+
async function fetchFreeCatalog(opts) {
|
|
200
|
+
if (!opts?.force && catalogCache && Date.now() - catalogCache.fetchedAt < exports.CATALOG_TTL_MS) {
|
|
201
|
+
return catalogCache.data;
|
|
202
|
+
}
|
|
203
|
+
const override = opts?.url || process.env.VECTOR_FREE_CATALOG_URL;
|
|
204
|
+
const candidates = override ? [override] : [exports.FREE_GATEWAY_CATALOG_URL, exports.FREE_CATALOG_URL];
|
|
205
|
+
for (const url of candidates) {
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(opts?.timeoutMs ?? 8_000) });
|
|
208
|
+
if (!res.ok)
|
|
209
|
+
continue;
|
|
210
|
+
const data = await res.json();
|
|
211
|
+
if (!isValidCatalog(data))
|
|
212
|
+
continue;
|
|
213
|
+
catalogCache = { data, fetchedAt: Date.now() };
|
|
214
|
+
return data;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
/* try the next source */
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Adopt a fetched catalog: prime the cache (freeProviderDef follows it) and
|
|
224
|
+
* refresh the free provider entry in the config. Returns true when the
|
|
225
|
+
* config object was modified (caller decides whether to persist).
|
|
226
|
+
*/
|
|
227
|
+
function applyFreeCatalog(data, config) {
|
|
228
|
+
catalogCache = { data, fetchedAt: Date.now() };
|
|
229
|
+
return ensureFreeProvider(config);
|
|
230
|
+
}
|