agen-vektor 0.3.30 → 0.3.32
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/agent/checkpoints.js +204 -20
- package/dist/agent/loop.js +25 -11
- package/dist/tools/semantic-search.js +276 -0
- package/dist/tui/chat.js +76 -25
- 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)(),
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.takeCheckpoint = takeCheckpoint;
|
|
37
|
+
exports.takeCheckpointAsync = takeCheckpointAsync;
|
|
37
38
|
exports.listCheckpoints = listCheckpoints;
|
|
38
39
|
exports.restoreCheckpoint = restoreCheckpoint;
|
|
39
40
|
exports.pruneCheckpoints = pruneCheckpoints;
|
|
@@ -63,6 +64,17 @@ exports.resetCheckpoints = resetCheckpoints;
|
|
|
63
64
|
* - Everything is best-effort: a missing git binary or a read-only home must
|
|
64
65
|
* NEVER break a run. All failures resolve to null/empty and the run
|
|
65
66
|
* proceeds without a safety net.
|
|
67
|
+
*
|
|
68
|
+
* Latency rules (bug "mentok di Edit", 2026-09-15):
|
|
69
|
+
* - The agent loop uses takeCheckpointAsync (off the event loop) so the TUI
|
|
70
|
+
* keeps painting while a big tree is scanned; the tool card renders BEFORE
|
|
71
|
+
* the snapshot starts (loop.ts), never behind it.
|
|
72
|
+
* - node_modules / .vector / .git are excluded from BOTH the size-count walk
|
|
73
|
+
* and the actual `git add` (pathspec excludes) — a bare /root work-tree
|
|
74
|
+
* used to stage 84k files ≈ 41s per snapshot and ballooned the shadow repo
|
|
75
|
+
* to 521 MB.
|
|
76
|
+
* - Work-trees above CP_MAX_FILES candidate files skip the snapshot entirely:
|
|
77
|
+
* the net costs more than it protects there.
|
|
66
78
|
*/
|
|
67
79
|
const node_child_process_1 = require("node:child_process");
|
|
68
80
|
const fs = __importStar(require("node:fs"));
|
|
@@ -82,9 +94,122 @@ function gitDirFor(cwd) {
|
|
|
82
94
|
return path.join((0, paths_1.getVectorDir)(), 'checkpoints', slug);
|
|
83
95
|
}
|
|
84
96
|
function git(gitDir, args, workTree) {
|
|
85
|
-
const r = (0, node_child_process_1.spawnSync)(GIT, [`--git-dir=${gitDir}`, ...(workTree ? [`--work-tree=${workTree}`] : []), ...args],
|
|
97
|
+
const r = (0, node_child_process_1.spawnSync)(GIT, [`--git-dir=${gitDir}`, ...(workTree ? [`--work-tree=${workTree}`] : []), ...args],
|
|
98
|
+
// cwd MUST be the work-tree when one is set: git resolves relative
|
|
99
|
+
// pathspecs (our `:(exclude)` patterns) against the process cwd — from a
|
|
100
|
+
// different directory the excludes silently stop matching (probe-proven).
|
|
101
|
+
{ encoding: 'utf8', timeout: 20_000, ...(workTree ? { cwd: workTree } : {}) });
|
|
86
102
|
return { ok: r.status === 0, out: (r.stdout || '') + (r.stderr || '') };
|
|
87
103
|
}
|
|
104
|
+
/** Async variant of `git(...)` — the child runs off the event loop so the
|
|
105
|
+
* TUI keeps rendering while a big snapshot scans the tree. 120s timeout
|
|
106
|
+
* (a slow disk scanning 100k+ files must not wedge the run forever). */
|
|
107
|
+
function gitAsync(gitDir, args, workTree) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
let child;
|
|
110
|
+
try {
|
|
111
|
+
child = (0, node_child_process_1.spawn)(GIT, [`--git-dir=${gitDir}`, ...(workTree ? [`--work-tree=${workTree}`] : []), ...args],
|
|
112
|
+
// Same cwd rule as git(): relative pathspecs resolve against cwd.
|
|
113
|
+
{ stdio: ['ignore', 'pipe', 'pipe'], ...(workTree ? { cwd: workTree } : {}) });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
resolve({ ok: false, out: 'spawn failed' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let out = '';
|
|
120
|
+
let done = false;
|
|
121
|
+
const finish = (ok, tail) => {
|
|
122
|
+
if (done)
|
|
123
|
+
return;
|
|
124
|
+
done = true;
|
|
125
|
+
resolve({ ok, out: out.slice(-4000) + tail });
|
|
126
|
+
};
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
try {
|
|
129
|
+
child.kill('SIGKILL');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* ignore */
|
|
133
|
+
}
|
|
134
|
+
finish(false, '\ngit timeout');
|
|
135
|
+
}, 120_000);
|
|
136
|
+
child.stdout?.on('data', (d) => {
|
|
137
|
+
out += d.toString();
|
|
138
|
+
if (out.length > 4096)
|
|
139
|
+
out = out.slice(-2048); // bounded
|
|
140
|
+
});
|
|
141
|
+
child.stderr?.on('data', (d) => {
|
|
142
|
+
out += d.toString();
|
|
143
|
+
if (out.length > 4096)
|
|
144
|
+
out = out.slice(-2048);
|
|
145
|
+
});
|
|
146
|
+
child.on('error', () => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
finish(false, '\nspawn error');
|
|
149
|
+
});
|
|
150
|
+
child.on('close', (code) => {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
finish(code === 0, code === 0 ? '' : `\nexit ${code}`);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Folders the safety net should NEVER track even when the project has no
|
|
158
|
+
* .gitignore of its own: dependency folders and vector's own state (a
|
|
159
|
+
* ~/.vector tree inside the work-tree would make the snapshot
|
|
160
|
+
* self-referential and explode the object store — seen live: 521 MB shadow
|
|
161
|
+
* repo for /root).
|
|
162
|
+
*/
|
|
163
|
+
const CP_EXCLUDE = ['node_modules', '.vector', '.git'];
|
|
164
|
+
/** Upper bound on work-tree scan size: beyond this, snapshotting takes
|
|
165
|
+
* minutes (VPS bench: 84k files = 41s per run) and the safety net costs
|
|
166
|
+
* more than it protects — skip with a clear reason instead. */
|
|
167
|
+
const CP_MAX_FILES = 20_000;
|
|
168
|
+
function countCandidateFiles(cwd) {
|
|
169
|
+
try {
|
|
170
|
+
let n = 0;
|
|
171
|
+
const walk = (dir, depth) => {
|
|
172
|
+
if (depth > 12 || n > CP_MAX_FILES)
|
|
173
|
+
return;
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
if (CP_EXCLUDE.includes(e.name))
|
|
183
|
+
continue;
|
|
184
|
+
if (e.isDirectory())
|
|
185
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
186
|
+
else if (e.isFile()) {
|
|
187
|
+
n++;
|
|
188
|
+
if (n > CP_MAX_FILES)
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
walk(cwd, 0);
|
|
194
|
+
return n;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** `git add -A` arguments with the exclude pathspecs applied. Runs with
|
|
201
|
+
* cwd = work-tree (see git()/gitAsync()), so the excludes are RELATIVE to
|
|
202
|
+
* the work-tree root: the bare name excludes the top-level folder and the
|
|
203
|
+
* trailing-slash glob excludes nested ones (git 2.43 probe-verified — the
|
|
204
|
+
* glob alone misses the root folder). */
|
|
205
|
+
function addArgs() {
|
|
206
|
+
const excludes = [];
|
|
207
|
+
for (const d of CP_EXCLUDE) {
|
|
208
|
+
excludes.push(`:(exclude)${d}`);
|
|
209
|
+
excludes.push(`:(exclude)**/${d}/`);
|
|
210
|
+
}
|
|
211
|
+
return ['add', '-A', '--', '.', ...excludes];
|
|
212
|
+
}
|
|
88
213
|
function ensureShadowRepo(gitDir) {
|
|
89
214
|
try {
|
|
90
215
|
if (!fs.existsSync(path.join(gitDir, 'HEAD'))) {
|
|
@@ -104,9 +229,24 @@ function ensureShadowRepo(gitDir) {
|
|
|
104
229
|
return false;
|
|
105
230
|
}
|
|
106
231
|
}
|
|
232
|
+
function stampRef(reason, task) {
|
|
233
|
+
const stamp = new Date();
|
|
234
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
235
|
+
const t = `${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}` +
|
|
236
|
+
`-${pad(stamp.getHours())}${pad(stamp.getMinutes())}${pad(stamp.getSeconds())}`;
|
|
237
|
+
const suffix = (cpSeq++).toString(36).padStart(4, '0');
|
|
238
|
+
const ref = `cp-${t}-${suffix}`;
|
|
239
|
+
const msg = `checkpoint ${ref} (${reason}) ${task.slice(0, 80)}`.trim();
|
|
240
|
+
return { ref, time: stamp.toISOString(), msg, stamp };
|
|
241
|
+
}
|
|
242
|
+
function parseHash(out) {
|
|
243
|
+
const hash = out.trim().split('\n')[0]?.trim() ?? '';
|
|
244
|
+
return /^[0-9a-f]{40}$/.test(hash) ? hash : null;
|
|
245
|
+
}
|
|
107
246
|
/**
|
|
108
|
-
* Take a checkpoint. Returns null when snapshotting is
|
|
109
|
-
* must treat that as "no safety net", never as an
|
|
247
|
+
* Take a checkpoint (SYNC legacy path). Returns null when snapshotting is
|
|
248
|
+
* impossible — callers must treat that as "no safety net", never as an
|
|
249
|
+
* error. The agent loop uses the async variant below.
|
|
110
250
|
*/
|
|
111
251
|
function takeCheckpoint(cwd, reason, task = '') {
|
|
112
252
|
try {
|
|
@@ -115,35 +255,79 @@ function takeCheckpoint(cwd, reason, task = '') {
|
|
|
115
255
|
const gitDir = gitDirFor(cwd);
|
|
116
256
|
if (!ensureShadowRepo(gitDir))
|
|
117
257
|
return null;
|
|
118
|
-
// -A stages the full tree incl. deletions and untracked files
|
|
119
|
-
// .gitignore in the user's
|
|
120
|
-
//
|
|
121
|
-
// documented trade-off, same as
|
|
122
|
-
|
|
258
|
+
// -A stages the full tree incl. deletions and untracked files, minus the
|
|
259
|
+
// CP_EXCLUDE folders (pathspec excludes). A .gitignore in the user's
|
|
260
|
+
// project is respected (their intent), but that also means ignored files
|
|
261
|
+
// are NOT protected by checkpoints — documented trade-off, same as
|
|
262
|
+
// Claude Code.
|
|
263
|
+
if (!git(gitDir, addArgs(), cwd).ok)
|
|
123
264
|
return null;
|
|
124
265
|
const tree = git(gitDir, ['write-tree']);
|
|
125
266
|
if (!tree.ok)
|
|
126
267
|
return null;
|
|
127
|
-
const treeHash = tree.out
|
|
128
|
-
if (
|
|
268
|
+
const treeHash = parseHash(tree.out);
|
|
269
|
+
if (!treeHash)
|
|
129
270
|
return null;
|
|
130
|
-
const
|
|
131
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
132
|
-
const t = `${stamp.getFullYear()}${pad(stamp.getMonth() + 1)}${pad(stamp.getDate())}` +
|
|
133
|
-
`-${pad(stamp.getHours())}${pad(stamp.getMinutes())}${pad(stamp.getSeconds())}`;
|
|
134
|
-
const suffix = (cpSeq++).toString(36).padStart(4, '0');
|
|
135
|
-
const ref = `cp-${t}-${suffix}`;
|
|
136
|
-
const msg = `checkpoint ${ref} (${reason}) ${task.slice(0, 80)}`.trim();
|
|
271
|
+
const { ref, time, msg } = stampRef(reason, task);
|
|
137
272
|
// Independent root commit — commit-tree gives full control over parents.
|
|
138
273
|
const commit = git(gitDir, ['commit-tree', treeHash, '-m', msg]);
|
|
139
274
|
if (!commit.ok)
|
|
140
275
|
return null;
|
|
141
|
-
const hash = commit.out
|
|
142
|
-
if (
|
|
276
|
+
const hash = parseHash(commit.out);
|
|
277
|
+
if (!hash)
|
|
143
278
|
return null;
|
|
144
279
|
if (!git(gitDir, ['update-ref', `${REF_PREFIX}${ref}`, hash]).ok)
|
|
145
280
|
return null;
|
|
146
|
-
return { ref, time
|
|
281
|
+
return { ref, time, reason, task: task.slice(0, 80), hash };
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* ASYNC checkpoint for the agent loop: same guarantees as takeCheckpoint,
|
|
289
|
+
* but the git child processes run off the event loop so the TUI keeps
|
|
290
|
+
* painting (Working line, spinner) while a big tree is scanned. Additional
|
|
291
|
+
* guards the sync version never had:
|
|
292
|
+
* - skip when the work-tree exceeds CP_MAX_FILES candidate files,
|
|
293
|
+
* - prune old refs after a successful snapshot so the shadow repo stays
|
|
294
|
+
* bounded (seen live: 521 MB after a week without pruning).
|
|
295
|
+
*/
|
|
296
|
+
async function takeCheckpointAsync(cwd, reason, task = '') {
|
|
297
|
+
try {
|
|
298
|
+
if (!fs.existsSync(cwd))
|
|
299
|
+
return null;
|
|
300
|
+
const n = countCandidateFiles(cwd);
|
|
301
|
+
if (n !== null && n > CP_MAX_FILES)
|
|
302
|
+
return null;
|
|
303
|
+
const gitDir = gitDirFor(cwd);
|
|
304
|
+
if (!ensureShadowRepo(gitDir))
|
|
305
|
+
return null;
|
|
306
|
+
// -A stages the full tree incl. deletions and untracked files, minus
|
|
307
|
+
// CP_EXCLUDE via pathspec excludes (the candidate walk above is only
|
|
308
|
+
// the size guard; git itself must also skip the folders).
|
|
309
|
+
const add = await gitAsync(gitDir, addArgs(), cwd);
|
|
310
|
+
if (!add.ok)
|
|
311
|
+
return null;
|
|
312
|
+
const tree = await gitAsync(gitDir, ['write-tree']);
|
|
313
|
+
if (!tree.ok)
|
|
314
|
+
return null;
|
|
315
|
+
const treeHash = parseHash(tree.out);
|
|
316
|
+
if (!treeHash)
|
|
317
|
+
return null;
|
|
318
|
+
const { ref, time, msg } = stampRef(reason, task);
|
|
319
|
+
const commit = await gitAsync(gitDir, ['commit-tree', treeHash, '-m', msg]);
|
|
320
|
+
if (!commit.ok)
|
|
321
|
+
return null;
|
|
322
|
+
const hash = parseHash(commit.out);
|
|
323
|
+
if (!hash)
|
|
324
|
+
return null;
|
|
325
|
+
const upd = await gitAsync(gitDir, ['update-ref', `${REF_PREFIX}${ref}`, hash]);
|
|
326
|
+
if (!upd.ok)
|
|
327
|
+
return null;
|
|
328
|
+
// Bound the shadow repo: keep the newest MAX_CHECKPOINTS refs.
|
|
329
|
+
pruneCheckpoints(cwd);
|
|
330
|
+
return { ref, time, reason, task: task.slice(0, 80), hash };
|
|
147
331
|
}
|
|
148
332
|
catch {
|
|
149
333
|
return null;
|
package/dist/agent/loop.js
CHANGED
|
@@ -146,15 +146,25 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
146
146
|
// Best-effort: null = no net, the run proceeds regardless.
|
|
147
147
|
const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'delete_file', 'apply_patch', 'shell', 'git', 'run_in_background']);
|
|
148
148
|
let checkpointTaken = false;
|
|
149
|
+
// Snapshot is ASYNC and runs INSIDE runOne (after onToolCall): the tool
|
|
150
|
+
// card must appear IMMEDIATELY, never behind the snapshot. The old sync
|
|
151
|
+
// design (spawnSync before onToolCall) froze the TUI for the whole
|
|
152
|
+
// snapshot — on a big cwd that read as "mentok di Edit" for 40+ seconds
|
|
153
|
+
// (bench VPS: git add -A pada /root = 41s).
|
|
149
154
|
const maybeCheckpoint = (toolName) => {
|
|
150
155
|
if (checkpointTaken || !MUTATING_TOOLS.has(toolName))
|
|
151
|
-
return;
|
|
156
|
+
return Promise.resolve();
|
|
152
157
|
checkpointTaken = true;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
return (0, checkpoints_1.takeCheckpointAsync)(cwd, toolName, userRequest)
|
|
159
|
+
.then((cp) => {
|
|
160
|
+
if (cp) {
|
|
161
|
+
callbacks.onStatus?.(`Checkpoint siap (${cp.ref})`);
|
|
162
|
+
callbacks.onActivity?.('checkpoint', `safety snapshot ${cp.ref}`);
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.catch(() => {
|
|
166
|
+
/* best-effort — never break the run */
|
|
167
|
+
});
|
|
158
168
|
};
|
|
159
169
|
const checkAbort = () => {
|
|
160
170
|
if (signal?.aborted) {
|
|
@@ -399,7 +409,10 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
399
409
|
'output_from_background',
|
|
400
410
|
]);
|
|
401
411
|
const runOne = async (tc) => {
|
|
412
|
+
// Tool card FIRST (visible instantly), snapshot AFTER — the card
|
|
413
|
+
// must never wait for the checkpoint git scan.
|
|
402
414
|
callbacks.onToolCall?.(tc.name, tc.arguments, tc.id);
|
|
415
|
+
const cpPromise = maybeCheckpoint(tc.name);
|
|
403
416
|
let toolResult;
|
|
404
417
|
try {
|
|
405
418
|
toolResult = await tools.execute(tc.name, tc.arguments, {
|
|
@@ -416,6 +429,9 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
416
429
|
// as a failed result so the TUI can finalize the card.
|
|
417
430
|
toolResult = { output: `ERROR: ${err.message}`, summary: 'tool error' };
|
|
418
431
|
}
|
|
432
|
+
// The run waits for the (async) safety net before the next model
|
|
433
|
+
// step, but the tool card above is already visible.
|
|
434
|
+
await cpPromise;
|
|
419
435
|
return { tc, toolResult };
|
|
420
436
|
};
|
|
421
437
|
try {
|
|
@@ -437,11 +453,9 @@ async function runAgentLoop(userRequest, opts, callbacks = {}) {
|
|
|
437
453
|
for (const batch of batches) {
|
|
438
454
|
checkAbort();
|
|
439
455
|
const parallel = batch.length > 1 && batch.every((tc) => PARALLEL_TOOLS.has(tc.name));
|
|
440
|
-
// Safety net
|
|
441
|
-
//
|
|
442
|
-
// never trigger it.
|
|
443
|
-
if (!parallel)
|
|
444
|
-
maybeCheckpoint(batch[0].name);
|
|
456
|
+
// Safety net moved INSIDE runOne (after onToolCall) so the tool
|
|
457
|
+
// card renders instantly; armed once per run, lazily. Parallel
|
|
458
|
+
// batches are read-only by construction and never trigger it.
|
|
445
459
|
toolCalls += batch.length;
|
|
446
460
|
if (!parallel) {
|
|
447
461
|
callbacks.onStatus?.(`Running ${batch[0].name}`);
|
|
@@ -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/dist/tui/chat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LOGO_SHEEN_MAX = exports.LOGO_SHEEN_INTERVAL_MS = exports.LOGO_SHEEN_STEP = exports.LOGO_FULL_MIN_COLS = exports.SIDE_GUTTER = exports.mergeStreamDelta = void 0;
|
|
3
|
+
exports.LOGO_SHEEN_MAX = exports.LOGO_SHEEN_INTERVAL_MS = exports.LOGO_SHEEN_STEP = exports.LOGO_FULL_ASCII_MIN_COLS = exports.LOGO_FULL_MIN_COLS = exports.SIDE_GUTTER = exports.mergeStreamDelta = void 0;
|
|
4
4
|
exports.toolIcon = toolIcon;
|
|
5
5
|
exports.toolLabel = toolLabel;
|
|
6
6
|
exports.followupsGroupOf = followupsGroupOf;
|
|
@@ -1484,31 +1484,75 @@ function clampScroll(offset, messages, width, height) {
|
|
|
1484
1484
|
/**
|
|
1485
1485
|
* Full-width VECTORHEAD ASCII logo — font "ANSI Shadow", the same figlet
|
|
1486
1486
|
* font Freebuff uses for its logo (CodebuffAI/freebuff
|
|
1487
|
-
* `cli/src/login/constants.ts` LOGO). All 6 rows are exactly
|
|
1488
|
-
* (
|
|
1487
|
+
* `cli/src/login/constants.ts` LOGO). All 6 rows are exactly 56 columns
|
|
1488
|
+
* (letters V=9 E=8 C=8 T=9 O=9 R=8, joined by 1 space) so the art is
|
|
1489
1489
|
* column-aligned and never wraps. Coloring follows Freebuff's use-logo
|
|
1490
1490
|
* rules with a VectorHead accent: solid blocks (█) render in `logoBlock`
|
|
1491
1491
|
* (white), shadow/border box-drawing chars render in orange #FF8534.
|
|
1492
|
+
*
|
|
1493
|
+
* ASCII safe mode (VECTOR_ASCII=1) uses DEDICATED plain-ASCII art instead
|
|
1494
|
+
* (VECTORHEAD_LOGO_ASCII below, figlet "Standard") — squeezing THIS font
|
|
1495
|
+
* 1:1 (█→#, ╔→+) keeps widths exact but destroys the letterforms.
|
|
1492
1496
|
*/
|
|
1493
1497
|
const VECTORHEAD_LOGO = [
|
|
1494
|
-
'██╗
|
|
1495
|
-
'██║
|
|
1496
|
-
'██║
|
|
1497
|
-
'╚██╗
|
|
1498
|
-
' ╚████╔╝
|
|
1499
|
-
' ╚═══╝
|
|
1498
|
+
'██╗ ██╗ ███████╗ ██████╗ ████████╗ ██████╗ ██████╗ ',
|
|
1499
|
+
'██║ ██║ ██╔════╝ ██╔════╝ ╚══██╔══╝ ██╔═══██╗ ██╔══██╗',
|
|
1500
|
+
'██║ ██║ █████╗ ██║ ██║ ██║ ██║ ██████╔╝',
|
|
1501
|
+
'╚██╗ ██╔╝ ██╔══╝ ██║ ██║ ██║ ██║ ██╔══██╗',
|
|
1502
|
+
' ╚████╔╝ ███████╗ ╚██████╗ ██║ ╚██████╔╝ ██║ ██║',
|
|
1503
|
+
' ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝',
|
|
1504
|
+
];
|
|
1505
|
+
/** Minimum terminal width for the full logo (~56 cols + margin). */
|
|
1506
|
+
exports.LOGO_FULL_MIN_COLS = 62;
|
|
1507
|
+
/** Minimum terminal width for the full ASCII (figlet) logo — 64 cols + margin. */
|
|
1508
|
+
exports.LOGO_FULL_ASCII_MIN_COLS = 68;
|
|
1509
|
+
/**
|
|
1510
|
+
* Plain-ASCII logo (figlet "Standard") for ASCII safe mode — real ASCII art
|
|
1511
|
+
* designed as art, NOT the block font squeezed char-per-char. All 6 rows
|
|
1512
|
+
* exactly 64 columns; pure \x20-\x7E so it renders 1 column per char on
|
|
1513
|
+
* EVERY font (the whole point of ASCII mode).
|
|
1514
|
+
*/
|
|
1515
|
+
const VECTORHEAD_LOGO_ASCII = [
|
|
1516
|
+
' __ _______ ____ _____ ___ ____ _ _ _____ _ ____ ',
|
|
1517
|
+
' \\ \\ / / ____/ ___|_ _/ _ \\| _ \\| | | | ____| / \\ | _ \\ ',
|
|
1518
|
+
' \\ \\ / /| _|| | | || | | | |_) | |_| | _| / _ \\ | | | |',
|
|
1519
|
+
' \\ V / | |__| |___ | || |_| | _ <| _ | |___ / ___ \\| |_| |',
|
|
1520
|
+
' \\_/ |_____\\____| |_| \\___/|_| \\ \\_| |_|_____/_/ \\ \\____/ ',
|
|
1521
|
+
' ',
|
|
1522
|
+
];
|
|
1523
|
+
/** Compact "VE" plain-ASCII logo (figlet "Standard") — 16 cols. */
|
|
1524
|
+
const VECTORHEAD_LOGO_SMALL_ASCII = [
|
|
1525
|
+
' __ _______ ',
|
|
1526
|
+
' \\ \\ / / ____|',
|
|
1527
|
+
' \\ \\ / /| _| ',
|
|
1528
|
+
' \\ V / | |___ ',
|
|
1529
|
+
' \\_/ |_____|',
|
|
1530
|
+
' ',
|
|
1500
1531
|
];
|
|
1501
|
-
/**
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
*
|
|
1532
|
+
/**
|
|
1533
|
+
* Pick the logo art + its full-tier width threshold for this terminal:
|
|
1534
|
+
* dedicated figlet ASCII art in ASCII safe mode, the ANSI Shadow block
|
|
1535
|
+
* font otherwise (Freebuff-style tiers).
|
|
1536
|
+
*/
|
|
1537
|
+
function logoArt(width) {
|
|
1538
|
+
if ((0, terminal_1.asciiModeEnabled)()) {
|
|
1539
|
+
return width >= exports.LOGO_FULL_ASCII_MIN_COLS
|
|
1540
|
+
? { rows: VECTORHEAD_LOGO_ASCII, minCols: exports.LOGO_FULL_ASCII_MIN_COLS }
|
|
1541
|
+
: { rows: VECTORHEAD_LOGO_SMALL_ASCII, minCols: 20 };
|
|
1542
|
+
}
|
|
1543
|
+
return width >= exports.LOGO_FULL_MIN_COLS
|
|
1544
|
+
? { rows: VECTORHEAD_LOGO, minCols: exports.LOGO_FULL_MIN_COLS }
|
|
1545
|
+
: { rows: VECTORHEAD_LOGO_SMALL, minCols: 20 };
|
|
1546
|
+
}
|
|
1547
|
+
/** Compact "VE" logo (Freebuff LOGO_SMALL tier: same font, letters joined
|
|
1548
|
+
* by 1 space — all 6 rows exactly 18 columns, column-aligned). */
|
|
1505
1549
|
const VECTORHEAD_LOGO_SMALL = [
|
|
1506
|
-
'██╗
|
|
1507
|
-
'██║
|
|
1508
|
-
'██║
|
|
1509
|
-
'╚██╗
|
|
1510
|
-
'
|
|
1511
|
-
' ╚═══╝
|
|
1550
|
+
'██╗ ██╗ ███████╗',
|
|
1551
|
+
'██║ ██║ ██╔════╝',
|
|
1552
|
+
'██║ ██║ █████╗ ',
|
|
1553
|
+
'╚██╗ ██╔╝ ██╔══╝ ',
|
|
1554
|
+
' ╚████╔╝ ███████╗',
|
|
1555
|
+
' ╚═══╝ ╚══════╝',
|
|
1512
1556
|
];
|
|
1513
1557
|
/** Shadow/border logo characters — Freebuff SHADOW_CHARS (login/constants.ts). */
|
|
1514
1558
|
const SHADOW_CHARS = new Set(['╚', '═', '╝', '║', '╔', '╗', '╠', '╣', '╦', '╩', '╬']);
|
|
@@ -1540,8 +1584,14 @@ function logoFgFor(char, charIndex, sheen) {
|
|
|
1540
1584
|
}
|
|
1541
1585
|
return theme_1.THEME.textBright;
|
|
1542
1586
|
}
|
|
1543
|
-
/** Paint one logo line with per-character colors (optional sheen sweep).
|
|
1587
|
+
/** Paint one logo line with per-character colors (optional sheen sweep).
|
|
1588
|
+
* Plain-ASCII art rows (no block/shadow glyphs) render single-tone — the
|
|
1589
|
+
* two-tone painting below is defined for the block font only. */
|
|
1544
1590
|
function paintLogoLine(line, sheen) {
|
|
1591
|
+
if (!/[█║═╔╗╚╝]/.test(line)) {
|
|
1592
|
+
// figlet ASCII art row: understated single-tone brand mark.
|
|
1593
|
+
return `${theme_1.THEME.muted}${line}${theme_1.THEME.reset}`;
|
|
1594
|
+
}
|
|
1545
1595
|
let out = '';
|
|
1546
1596
|
let color = '';
|
|
1547
1597
|
for (let i = 0; i < line.length; i++) {
|
|
@@ -1575,9 +1625,10 @@ function paintLogoLine(line, sheen) {
|
|
|
1575
1625
|
function buildWelcomeBlock(width, cwd, sheen = null) {
|
|
1576
1626
|
const lines = [];
|
|
1577
1627
|
// Freebuff use-logo tiers: full logo when wide, compact logo when medium,
|
|
1578
|
-
// single-line wordmark on very narrow terminals
|
|
1579
|
-
|
|
1580
|
-
|
|
1628
|
+
// single-line wordmark on very narrow terminals (art picked by logoArt —
|
|
1629
|
+
// dedicated figlet ASCII in ASCII safe mode, block font otherwise).
|
|
1630
|
+
const { rows: logo, minCols } = logoArt(width);
|
|
1631
|
+
if (width >= minCols) {
|
|
1581
1632
|
for (const l of logo)
|
|
1582
1633
|
lines.push(paintLogoLine(l, sheen));
|
|
1583
1634
|
}
|
|
@@ -1600,8 +1651,8 @@ function buildWelcomeBlock(width, cwd, sheen = null) {
|
|
|
1600
1651
|
function renderWelcome(width, height, cwd, opts = {}) {
|
|
1601
1652
|
const sheen = opts.sheen ?? null;
|
|
1602
1653
|
const lines = [];
|
|
1603
|
-
const
|
|
1604
|
-
if (width >=
|
|
1654
|
+
const { rows: logo, minCols } = logoArt(width);
|
|
1655
|
+
if (width >= minCols) {
|
|
1605
1656
|
for (const l of logo)
|
|
1606
1657
|
lines.push(paintLogoLine(l, sheen));
|
|
1607
1658
|
}
|
package/package.json
CHANGED