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 CHANGED
@@ -1,15 +1,23 @@
1
- # VectorHead
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
+ ![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20Termux-9EFC62?style=for-the-badge&labelColor=0b0f14)
14
+ ![Node](https://img.shields.io/badge/node-20%2B-339933?style=for-the-badge&labelColor=0b0f14)
15
+ ![License](https://img.shields.io/badge/license-MIT-3B82F6?style=for-the-badge&labelColor=0b0f14)
16
+ [![Tests](https://img.shields.io/github/actions/workflow/status/clickmamaheti-prog/vector-agent/ci.yml?style=for-the-badge&label=tests&labelColor=0b0f14)](https://github.com/clickmamaheti-prog/vector-agent/actions/workflows/ci.yml)
17
+
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
- Dari source: clone repo ini, lalu `npm install && npm run build && npm link`
59
- (membuat `vector` tersedia di PATH).
72
+ Clone this repo, then:
60
73
 
61
- Then:
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
- > VectorHead has **zero runtime dependencies** and does not assume `systemd`, `sudo`, `apt`, or a desktop GUI — everything runs in a plain terminal.
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
 
@@ -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
- this.config = (0, config_1.effectiveConfig)(opts.config);
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).
@@ -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({
@@ -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 — e.g. "Saya dijalankan oleh model deepseek-ai/DeepSeek-V4-Flash lewat provider custom". Do not invent a different model name.`;
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). Modal overlays are absolutely positioned, so they force a
310
- // full repaint.
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
- const cursor = (0, terminal_1.cursorTo)(snap.inputRow, snap.cursorCol);
336
- if (cursor !== lastCursorPos)
337
- out.push(cursor);
338
- lastCursorPos = cursor;
339
- out.push(terminal_1.ANSI.showCursor);
340
- process.stdout.write(terminal_1.ANSI.hideCursor + out.join(''));
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();
@@ -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
- /** Create a key stream (async iterable of KeyEvent). */
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
- for (const ev of parseKey(data))
288
- onKey(ev);
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
  }
@@ -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
- permissionMode: 'ask',
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
+ }