agen-vektor 0.3.30 → 0.3.31
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/dist/agent/agent.js +4 -0
- package/dist/tools/semantic-search.js +276 -0
- package/package.json +1 -1
package/dist/agent/agent.js
CHANGED
|
@@ -5,6 +5,7 @@ const factory_1 = require("../providers/factory");
|
|
|
5
5
|
const registry_1 = require("../tools/registry");
|
|
6
6
|
const filesystem_1 = require("../tools/filesystem");
|
|
7
7
|
const search_1 = require("../tools/search");
|
|
8
|
+
const semantic_search_1 = require("../tools/semantic-search");
|
|
8
9
|
const shell_1 = require("../tools/shell");
|
|
9
10
|
const git_1 = require("../tools/git");
|
|
10
11
|
const web_1 = require("../tools/web");
|
|
@@ -59,6 +60,9 @@ class Agent {
|
|
|
59
60
|
this.tools.registerMany([
|
|
60
61
|
...(0, filesystem_1.createFilesystemTools)(),
|
|
61
62
|
...(0, search_1.createSearchTools)(),
|
|
63
|
+
// Semantic code search (via gateway embeddings relay): semantic_index /
|
|
64
|
+
// semantic_search — cari kode by MEANING, zero-config (key di gateway).
|
|
65
|
+
...(0, semantic_search_1.createSemanticSearchTools)(),
|
|
62
66
|
(0, shell_1.createShellTool)(),
|
|
63
67
|
(0, git_1.createGitTool)(),
|
|
64
68
|
...(0, web_1.createWebTools)(),
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createSemanticSearchTools = createSemanticSearchTools;
|
|
37
|
+
/**
|
|
38
|
+
* Semantic code search — "agent yang hafal codebase" (2026-09-14).
|
|
39
|
+
*
|
|
40
|
+
* Alur: chunk file proyek → embedding via GATEWAY relay (/v1/embeddings,
|
|
41
|
+
* zero-config & zero-key — key provider sk- hidup di gateway, pola sama
|
|
42
|
+
* dengan e2b.ts/web.ts) → simpan vektor di file lokal (~/.vector/index.json)
|
|
43
|
+
* → cosine similarity saat query → top-k potongan paling relevan.
|
|
44
|
+
*
|
|
45
|
+
* Zero dependency: embedding = HTTP call, similarity = dot product manual,
|
|
46
|
+
* storage = satu file JSON. Index dibangun manual via tool semantic_index
|
|
47
|
+
* (atau otomatis saat semantic_search pertama tanpa index).
|
|
48
|
+
*
|
|
49
|
+
* Batas wajar (anti-abuse & anti-bloat):
|
|
50
|
+
* - max file 400 (arg hingga 2000), skip >200KB & file binary-ish
|
|
51
|
+
* - max chunk 4000; batch embedding 16 input/request; jeda antar-batch
|
|
52
|
+
* 1.1s (rate limit gateway 60/menit — env VECTOR_EMBED_DELAY_MS utk test)
|
|
53
|
+
* - skip nama file sensitif (credential/secret/.env/key/pem) — index tidak
|
|
54
|
+
* boleh jadi salinan rahasia
|
|
55
|
+
*/
|
|
56
|
+
const fs = __importStar(require("node:fs"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
const free_tier_1 = require("../config/free-tier");
|
|
59
|
+
const paths_1 = require("../utils/paths");
|
|
60
|
+
const GW_BASE = free_tier_1.FREE_GATEWAY_URL.replace(/\/v1$/, '');
|
|
61
|
+
const EMBED_URL = GW_BASE + '/v1/embeddings';
|
|
62
|
+
const DEFAULT_EMBED_MODEL = 'text-embedding-v4';
|
|
63
|
+
const IGNORE = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', '.cache', '.next', 'target', 'coverage', 'sessions', 'logs', 'memory']);
|
|
64
|
+
const TEXT_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.md', '.py', '.go', '.rs', '.java', '.rb', '.php', '.sh', '.bash', '.yml', '.yaml', '.toml', '.html', '.css', '.scss', '.sql', '.c', '.h', '.cpp', '.hpp', '.cs', '.swift', '.kt', '.txt', '.dockerfile', '.ini', '.cfg']);
|
|
65
|
+
const SENSITIVE = /credential|secret|\.env|\.pem|\.key|id_rsa|password/i;
|
|
66
|
+
const MAX_FILE_BYTES = 200_000;
|
|
67
|
+
const MAX_FILES_DEFAULT = 400;
|
|
68
|
+
const MAX_FILES_HARD = 2000;
|
|
69
|
+
const MAX_CHUNKS = 4000;
|
|
70
|
+
const CHUNK_LINES = 64;
|
|
71
|
+
const BATCH = 16;
|
|
72
|
+
const DELAY_MS = Math.max(0, Number(process.env.VECTOR_EMBED_DELAY_MS ?? 1100));
|
|
73
|
+
function indexPath() {
|
|
74
|
+
return path.join((0, paths_1.getVectorDir)(), 'index.json');
|
|
75
|
+
}
|
|
76
|
+
function loadIndex() {
|
|
77
|
+
try {
|
|
78
|
+
const raw = fs.readFileSync(indexPath(), 'utf8');
|
|
79
|
+
const j = JSON.parse(raw);
|
|
80
|
+
return j && j.version === 1 && Array.isArray(j.chunks) ? j : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function saveIndex(idx) {
|
|
87
|
+
fs.mkdirSync((0, paths_1.getVectorDir)(), { recursive: true });
|
|
88
|
+
fs.writeFileSync(indexPath(), JSON.stringify(idx), { mode: 0o600 });
|
|
89
|
+
}
|
|
90
|
+
function walkFiles(dir, out, max) {
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const e of entries) {
|
|
99
|
+
if (out.length >= max)
|
|
100
|
+
return;
|
|
101
|
+
if (IGNORE.has(e.name) || e.name.startsWith('.'))
|
|
102
|
+
continue;
|
|
103
|
+
if (SENSITIVE.test(e.name))
|
|
104
|
+
continue;
|
|
105
|
+
const full = path.join(dir, e.name);
|
|
106
|
+
if (e.isDirectory())
|
|
107
|
+
walkFiles(full, out, max);
|
|
108
|
+
else if (TEXT_EXT.has(path.extname(e.name).toLowerCase()) || e.name === 'Dockerfile')
|
|
109
|
+
out.push(full);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function chunkText(content) {
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
const chunks = [];
|
|
115
|
+
for (let i = 0; i < lines.length && chunks.length < MAX_CHUNKS; i += CHUNK_LINES) {
|
|
116
|
+
const slice = lines.slice(i, i + CHUNK_LINES);
|
|
117
|
+
const text = slice.join('\n').trim();
|
|
118
|
+
if (text)
|
|
119
|
+
chunks.push({ start: i + 1, end: i + slice.length, text });
|
|
120
|
+
}
|
|
121
|
+
return chunks;
|
|
122
|
+
}
|
|
123
|
+
async function embed(inputs, model) {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
const timer = setTimeout(() => controller.abort(), 30_000);
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(EMBED_URL, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': 'VectorHead-AI-Agent/0.1' },
|
|
131
|
+
body: JSON.stringify({ model, input: inputs }),
|
|
132
|
+
});
|
|
133
|
+
const data = await res.json().catch(() => null);
|
|
134
|
+
if (!res.ok || !data || !Array.isArray(data.data)) {
|
|
135
|
+
const snippet = data && data.error ? JSON.stringify(data.error).slice(0, 200) : `HTTP ${res.status}`;
|
|
136
|
+
throw new Error(`gateway embeddings gagal: ${snippet}`);
|
|
137
|
+
}
|
|
138
|
+
// OpenAI format: data[i].embedding — urut by index utk aman.
|
|
139
|
+
const sorted = [...data.data].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
140
|
+
return sorted.map((d) => d.embedding);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
147
|
+
function cosine(a, b) {
|
|
148
|
+
let dot = 0, na = 0, nb = 0;
|
|
149
|
+
const n = Math.min(a.length, b.length);
|
|
150
|
+
for (let i = 0; i < n; i++) {
|
|
151
|
+
dot += a[i] * b[i];
|
|
152
|
+
na += a[i] * a[i];
|
|
153
|
+
nb += b[i] * b[i];
|
|
154
|
+
}
|
|
155
|
+
return na && nb ? dot / (Math.sqrt(na) * Math.sqrt(nb)) : 0;
|
|
156
|
+
}
|
|
157
|
+
async function buildIndex(root, maxFiles) {
|
|
158
|
+
const files = [];
|
|
159
|
+
walkFiles(root, files, maxFiles);
|
|
160
|
+
if (!files.length)
|
|
161
|
+
return `ERROR: tidak ada file teks yang bisa di-index di ${root}`;
|
|
162
|
+
const model = DEFAULT_EMBED_MODEL;
|
|
163
|
+
const chunks = [];
|
|
164
|
+
for (const full of files) {
|
|
165
|
+
let content;
|
|
166
|
+
try {
|
|
167
|
+
if (fs.statSync(full).size > MAX_FILE_BYTES)
|
|
168
|
+
continue;
|
|
169
|
+
content = fs.readFileSync(full, 'utf8');
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (content.includes('\u0000'))
|
|
175
|
+
continue; // binary-ish
|
|
176
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
177
|
+
for (const c of chunkText(content))
|
|
178
|
+
chunks.push({ file: rel, ...c, embedding: [] });
|
|
179
|
+
}
|
|
180
|
+
if (!chunks.length)
|
|
181
|
+
return 'ERROR: tidak ada chunk yang bisa di-index';
|
|
182
|
+
for (let i = 0; i < chunks.length; i += BATCH) {
|
|
183
|
+
const vecs = await embed(chunks.slice(i, i + BATCH).map((c) => c.text), model);
|
|
184
|
+
if (vecs.length !== Math.min(BATCH, chunks.length - i)) {
|
|
185
|
+
return `ERROR: jumlah vektor upstream tidak cocok (${vecs.length})`;
|
|
186
|
+
}
|
|
187
|
+
vecs.forEach((v, j) => { chunks[i + j].embedding = v; });
|
|
188
|
+
if (i + BATCH < chunks.length && DELAY_MS)
|
|
189
|
+
await sleep(DELAY_MS);
|
|
190
|
+
}
|
|
191
|
+
saveIndex({ version: 1, model, builtAt: Date.now(), chunks });
|
|
192
|
+
const preview = [...new Set(chunks.map((c) => c.file))].slice(0, 5).join(', ');
|
|
193
|
+
return `OK: index dibangun — ${chunks.length} chunk dari ${files.length} file (model ${model}) → ${indexPath()}${preview ? '\nContoh: ' + preview : ''}`;
|
|
194
|
+
}
|
|
195
|
+
function searchIndex(idx, queryVec, k) {
|
|
196
|
+
const scored = idx.chunks
|
|
197
|
+
.map((c) => ({ c, score: cosine(queryVec, c.embedding) }))
|
|
198
|
+
.sort((a, b) => b.score - a.score)
|
|
199
|
+
.slice(0, Math.max(1, k));
|
|
200
|
+
if (!scored.length || scored[0].score <= 0) {
|
|
201
|
+
return 'Tidak ada hasil relevan (index kosong / skor 0) — coba semantic_index ulang.';
|
|
202
|
+
}
|
|
203
|
+
const lines = scored.map((s, i) => {
|
|
204
|
+
const head = s.c.text.replace(/\s+/g, ' ').slice(0, 180);
|
|
205
|
+
return `${i + 1}. ${s.c.file}:${s.c.start}-${s.c.end} (skor ${s.score.toFixed(3)})\n ${head}`;
|
|
206
|
+
});
|
|
207
|
+
return `Hasil semantic search (top ${scored.length} dari ${idx.chunks.length} chunk):\n${lines.join('\n')}`;
|
|
208
|
+
}
|
|
209
|
+
function createSemanticSearchTools() {
|
|
210
|
+
return [
|
|
211
|
+
{
|
|
212
|
+
definition: {
|
|
213
|
+
name: 'semantic_index',
|
|
214
|
+
description: 'Build/refresh the local semantic index: embed project files (via the VectorHead gateway, no API key needed) and store vectors in ~/.vector/index.json. Run once per project (or after big changes) — then semantic_search can find code by MEANING, not just keywords.',
|
|
215
|
+
parameters: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
path: { type: 'string', description: 'Directory to index, relative to project root (default ".")' },
|
|
219
|
+
max_files: { type: 'number', description: 'Max files to index (default 400, max 2000)' },
|
|
220
|
+
},
|
|
221
|
+
required: [],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
async execute(args, ctx) {
|
|
225
|
+
const base = path.resolve(ctx.cwd, String(args.path || '.'));
|
|
226
|
+
const max = Math.min(MAX_FILES_HARD, Math.max(1, Number(args.max_files) || MAX_FILES_DEFAULT));
|
|
227
|
+
try {
|
|
228
|
+
return { output: await buildIndex(base, max) };
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
return { output: 'ERROR: ' + String(e.message || e) };
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
definition: {
|
|
237
|
+
name: 'semantic_search',
|
|
238
|
+
description: 'Search the codebase by MEANING (semantic similarity over embedded chunks). Automatically builds the index first if none exists. Use for "where is the retry logic?" style questions where grep/keywords fail.',
|
|
239
|
+
parameters: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
query: { type: 'string', description: 'Natural-language query, e.g. "where do we validate the device id"' },
|
|
243
|
+
k: { type: 'number', description: 'Max results (default 5, max 20)' },
|
|
244
|
+
path: { type: 'string', description: 'Directory that was indexed (default ".")' },
|
|
245
|
+
},
|
|
246
|
+
required: ['query'],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
async execute(args, ctx) {
|
|
250
|
+
const query = String(args.query || '').trim();
|
|
251
|
+
if (!query)
|
|
252
|
+
return { output: 'ERROR: query wajib' };
|
|
253
|
+
const k = Math.min(20, Math.max(1, Number(args.k) || 5));
|
|
254
|
+
try {
|
|
255
|
+
let idx = loadIndex();
|
|
256
|
+
if (!idx) {
|
|
257
|
+
const base = path.resolve(ctx.cwd, String(args.path || '.'));
|
|
258
|
+
const built = await buildIndex(base, MAX_FILES_DEFAULT);
|
|
259
|
+
if (built.startsWith('ERROR'))
|
|
260
|
+
return { output: built };
|
|
261
|
+
idx = loadIndex();
|
|
262
|
+
if (!idx)
|
|
263
|
+
return { output: 'ERROR: index gagal dibaca setelah build' };
|
|
264
|
+
}
|
|
265
|
+
const [qv] = await embed([query], idx.model || DEFAULT_EMBED_MODEL);
|
|
266
|
+
if (!qv)
|
|
267
|
+
return { output: 'ERROR: embedding query kosong' };
|
|
268
|
+
return { output: searchIndex(idx, qv, k) };
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
return { output: 'ERROR: ' + String(e.message || e) };
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
}
|
package/package.json
CHANGED