agen-vektor 0.3.31 → 0.3.33
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 +5 -0
- package/dist/agent/checkpoints.js +204 -20
- package/dist/agent/loop.js +25 -11
- package/dist/cli/index.js +16 -1
- package/dist/cli/splash.js +167 -0
- package/dist/tui/chat.js +76 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -471,6 +471,11 @@ secrets stay private):
|
|
|
471
471
|
|
|
472
472
|
## Changelog
|
|
473
473
|
|
|
474
|
+
### 0.3.33
|
|
475
|
+
- **Boot splash "Loading System . . ." before the TUI dashboard.** Launching `vector` now shows a brief boot screen — brand banner, three loading steps (Config → Environment → Provider), a braille spinner and progress bar, then "System ready" — before the whole block is cleanly erased and the chat TUI enters alt-screen. Zero scrollback residue.
|
|
476
|
+
- **Zero dependencies, Termux-safe.** The splash follows `VECTOR_ASCII` (spinner becomes `|/-\` in ASCII safe mode), never blocks process exit (timer is unref'd), and failure to build the agent still clears the splash before the error propagates.
|
|
477
|
+
- **Controls:** `VECTOR_SPLASH=0` disables it entirely; `VECTOR_SPLASH_STEP_MS` (default 450) and `VECTOR_SPLASH_HOLD_MS` (default 900) tune the timing.
|
|
478
|
+
|
|
474
479
|
### 0.3.30
|
|
475
480
|
- **Thinking card matches Freebuff `thinking.tsx` exactly — no mid-run color flip-flop.** The reasoning body renders muted (`#acb3bf`) italic in BOTH streaming and completed states; only the header (dot + bold "Thinking") is foreground-white. Previously the body flipped from white (streaming) to muted (done), which read as the card changing color during a run.
|
|
476
481
|
- **Expanded thinking view is raw muted italic with word wrap.** The in-card markdown re-render was removed — headings/inline code no longer paint their own colors inside the card (the mixed-color expanded view read as noise). Markdown markers in reasoning now show as-is, uniformly styled.
|
|
@@ -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}`);
|
package/dist/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const factory_1 = require("../providers/factory");
|
|
|
22
22
|
const paths_1 = require("../utils/paths");
|
|
23
23
|
const logger_1 = require("../utils/logger");
|
|
24
24
|
const keyboard_1 = require("./keyboard");
|
|
25
|
+
const splash_1 = require("./splash");
|
|
25
26
|
const terminal_1 = require("../utils/terminal");
|
|
26
27
|
/** Disable auto-wrap margins (DECAWM off): a full-width row then CANNOT
|
|
27
28
|
* push the cursor into the pending-wrap state, so the next write can never
|
|
@@ -248,7 +249,21 @@ async function runTui(opts) {
|
|
|
248
249
|
await runNonInteractive(opts);
|
|
249
250
|
return;
|
|
250
251
|
}
|
|
251
|
-
|
|
252
|
+
// Boot splash "Loading System . . ." (additive, 2026-09-16): tampil
|
|
253
|
+
// sebelum buildAgent/loading dashboard; VECTOR_SPLASH=0 mematikan.
|
|
254
|
+
const splash = await (0, splash_1.runSplash)();
|
|
255
|
+
let agent;
|
|
256
|
+
try {
|
|
257
|
+
await splash.step('config dimuat');
|
|
258
|
+
agent = await buildAgent(opts);
|
|
259
|
+
await splash.step('environment siap');
|
|
260
|
+
await splash.step(`provider ${opts.provider || agent.config.provider}`);
|
|
261
|
+
await splash.finish();
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
await splash.finish();
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
252
267
|
const config = agent.config;
|
|
253
268
|
// OpenCode-style theming: --theme wins, then VECTOR_THEME (already folded
|
|
254
269
|
// into config via effectiveConfig inside the Agent), then config.json.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splashEnabled = splashEnabled;
|
|
4
|
+
exports.runSplash = runSplash;
|
|
5
|
+
/**
|
|
6
|
+
* Boot splash — "Loading System . . ." (2026-09-16).
|
|
7
|
+
*
|
|
8
|
+
* Additive fitur: ditampilkan HANYA di runTui (interactive TTY) sebelum
|
|
9
|
+
* dashboard chat TUI masuk alt-screen. Tidak menyentuh agent, provider,
|
|
10
|
+
* maupun TUI — murni penggambar ANSI di buffer utama lalu DIHAPUS BERSIH
|
|
11
|
+
* sebelum TUI mulai (tidak meninggalkan sampah scrollback).
|
|
12
|
+
*
|
|
13
|
+
* Zero dependency: hanya ANSI escape dari utils/terminal. ASCII safe mode
|
|
14
|
+
* (VECTOR_ASCII=1) otomatis: spinner & simbol di-ganti varian 1 kolom via
|
|
15
|
+
* toAsciiSafe agar tidak merobek layout di Termux/font CJK.
|
|
16
|
+
*
|
|
17
|
+
* Skip: VECTOR_SPLASH=0 mematikan splash sepenuhnya (zero delay) — cocok
|
|
18
|
+
* untuk skrip/otomasi. Step delay bisa diatur VECTOR_SPLASH_STEP_MS.
|
|
19
|
+
*/
|
|
20
|
+
const terminal_1 = require("../utils/terminal");
|
|
21
|
+
/** Spinner frames — braille di mode normal, 1-kolom ASCII di ASCII mode. */
|
|
22
|
+
const SPINNER_BRAILLE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
|
+
const SPINNER_ASCII = ['|', '/', '-', '\\'];
|
|
24
|
+
/** Warna brand (independen dari THEME — theme di-apply SETELAH buildAgent). */
|
|
25
|
+
const ORANGE = '\x1b[38;2;255;133;52m'; // #FF8534 — aksen VectorHead
|
|
26
|
+
const GREEN = '\x1b[38;2;159;252;98m'; // #9EFC62 — sukses VectorHead
|
|
27
|
+
const WHITE = '\x1b[97m';
|
|
28
|
+
const DIM = terminal_1.ANSI.dim;
|
|
29
|
+
const RESET = terminal_1.ANSI.reset;
|
|
30
|
+
/** Jeda per step agar urutan loading terasa (bisa di-override untuk test). */
|
|
31
|
+
const STEP_DELAY_MS = Math.max(0, Number(process.env.VECTOR_SPLASH_STEP_MS ?? 450));
|
|
32
|
+
/** Jeda "System ready" sebelum blok dihapus. */
|
|
33
|
+
const FINISH_HOLD_MS = Math.max(0, Number(process.env.VECTOR_SPLASH_HOLD_MS ?? 900));
|
|
34
|
+
/** Lebar progress bar (kolom). */
|
|
35
|
+
const BAR_WIDTH = 22;
|
|
36
|
+
/** Splash aktif? VECTOR_SPLASH=0 mematikan (env menang, default ON). */
|
|
37
|
+
function splashEnabled() {
|
|
38
|
+
const v = process.env['VECTOR_SPLASH'];
|
|
39
|
+
if (v === undefined)
|
|
40
|
+
return true;
|
|
41
|
+
return v !== '0' && v.toLowerCase() !== 'false';
|
|
42
|
+
}
|
|
43
|
+
/** Panjang terlihat (kolom) dari string ber-ANSI milik splash sendiri. */
|
|
44
|
+
function visibleLen(painted) {
|
|
45
|
+
return painted.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').length;
|
|
46
|
+
}
|
|
47
|
+
/** Bar progress "███░░░░░ 45%" — satu kolom per glyph di kedua mode. */
|
|
48
|
+
function bar(fraction) {
|
|
49
|
+
const pct = Math.max(0, Math.min(1, fraction));
|
|
50
|
+
const filled = Math.round(BAR_WIDTH * pct);
|
|
51
|
+
return (GREEN + '█'.repeat(filled) + DIM + '░'.repeat(BAR_WIDTH - filled) + RESET +
|
|
52
|
+
` ${Math.round(pct * 100)}%`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Jalankan splash: banner sekali + spinner live row. Panggil `step()` per
|
|
56
|
+
* progress, akhiri dengan `finish()` (WAJIB — mengembalikan kursor).
|
|
57
|
+
* Menangani sendiri kasus lebar terminal ekstrim (kolom < 24 → bar disembunyikan).
|
|
58
|
+
*/
|
|
59
|
+
async function runSplash() {
|
|
60
|
+
if (!splashEnabled()) {
|
|
61
|
+
// No-op handle: kode pemanggil tetap bisa memanggil step()/finish().
|
|
62
|
+
return { step: async () => { }, finish: async () => { } };
|
|
63
|
+
}
|
|
64
|
+
const out = process.stdout;
|
|
65
|
+
const cols = Math.max(1, (0, terminal_1.getTerminalSize)().cols);
|
|
66
|
+
const ascii = terminal_1.toAsciiSafe; // ASCII safe mode mengikuti VECTOR_ASCII/Termux
|
|
67
|
+
const frames = asciiModeFrames();
|
|
68
|
+
const showBar = cols >= BAR_WIDTH + 30;
|
|
69
|
+
// Pusatkan satu baris (diukur dari versi TERPAINT agar ASCII map ikut dihitung).
|
|
70
|
+
const center = (text) => {
|
|
71
|
+
const painted = ascii(text);
|
|
72
|
+
const pad = Math.max(0, Math.floor((cols - visibleLen(painted)) / 2));
|
|
73
|
+
return ' '.repeat(pad) + text;
|
|
74
|
+
};
|
|
75
|
+
const lines = []; // jumlah baris yang sudah digambar (untuk clear di finish)
|
|
76
|
+
const write = (s) => {
|
|
77
|
+
out.write(ascii(s));
|
|
78
|
+
};
|
|
79
|
+
const paintLine = (s) => {
|
|
80
|
+
lines.push(s);
|
|
81
|
+
write(center(s) + '\r\n');
|
|
82
|
+
};
|
|
83
|
+
out.write(terminal_1.ANSI.hideCursor);
|
|
84
|
+
// ── Banner (sekali) ──────────────────────────────────────────────
|
|
85
|
+
paintLine(`${ORANGE}▮${RESET} ${WHITE}VectorHead${RESET} ${DIM}v${version()}${RESET}`);
|
|
86
|
+
paintLine(`${DIM}Initializing system components . . .${RESET}`);
|
|
87
|
+
paintLine('');
|
|
88
|
+
// ── Live row (spinner + label + bar) — di-rewrite di tempat ──────
|
|
89
|
+
let frame = 0;
|
|
90
|
+
let stepIndex = 0;
|
|
91
|
+
let label = 'Loading System';
|
|
92
|
+
const renderLive = () => {
|
|
93
|
+
const glyph = frames[frame % frames.length];
|
|
94
|
+
frame++;
|
|
95
|
+
const pct = Math.min(0.92, 0.15 + stepIndex * 0.22);
|
|
96
|
+
const text = `${GREEN}${glyph}${RESET} ${WHITE}${label}${RESET} ${DIM}.${RESET}${DIM}.${RESET}${DIM}.${RESET}` +
|
|
97
|
+
(showBar ? ` ${bar(pct)}` : '');
|
|
98
|
+
// Rewrite in place: kembali ke awal baris live lalu gambar ulang.
|
|
99
|
+
write('\r' + terminal_1.ANSI.clearLineEnd + center(text));
|
|
100
|
+
};
|
|
101
|
+
const liveTimer = setInterval(renderLive, 90);
|
|
102
|
+
// Timer TIDAK boleh menahan proses kalau splash ternyata tidak di-finish
|
|
103
|
+
// (crash path) — biarkan Node berhemi natural.
|
|
104
|
+
if (typeof liveTimer.unref === 'function')
|
|
105
|
+
liveTimer.unref();
|
|
106
|
+
renderLive();
|
|
107
|
+
await sleep(420); // momen awal "Loading System . . ." terlihat jelas
|
|
108
|
+
const names = ['Config', 'Environment', 'Provider'];
|
|
109
|
+
return {
|
|
110
|
+
async step(detail) {
|
|
111
|
+
const name = names[Math.min(stepIndex, names.length - 1)];
|
|
112
|
+
stepIndex++;
|
|
113
|
+
// Ganti live row dengan baris step selesai (append permanen).
|
|
114
|
+
const mark = ascii('✓') === '✓' ? '✓' : 'ok';
|
|
115
|
+
const check = `${GREEN}${mark}${RESET}`;
|
|
116
|
+
const info = detail ? ` ${DIM}${detail}${RESET}` : '';
|
|
117
|
+
write('\r' + terminal_1.ANSI.clearLineEnd); // hapus live row
|
|
118
|
+
paintLine(`${check} ${WHITE}${name}${RESET}${info}`);
|
|
119
|
+
label = `Loading ${names[Math.min(stepIndex, names.length - 1)] ?? 'System'}`;
|
|
120
|
+
renderLive();
|
|
121
|
+
await sleep(STEP_DELAY_MS);
|
|
122
|
+
},
|
|
123
|
+
async finish() {
|
|
124
|
+
clearInterval(liveTimer);
|
|
125
|
+
const mark = ascii('✓') === '✓' ? '✓' : 'ok';
|
|
126
|
+
write('\r' + terminal_1.ANSI.clearLineEnd);
|
|
127
|
+
paintLine(`${GREEN}${mark}${RESET} ${WHITE}System ready${RESET}`);
|
|
128
|
+
write(`\r\n${DIM}Starting VectorHead TUI . . .${RESET}`);
|
|
129
|
+
lines.push('', '');
|
|
130
|
+
await sleep(FINISH_HOLD_MS);
|
|
131
|
+
// Hapus seluruh blok splash → terminal bersih sebelum alt-screen TUI.
|
|
132
|
+
// Kursor berada DI baris terakhir (Starting…) → clearLine dulu, lalu
|
|
133
|
+
// naik + clear per baris tergambar (banner s.d. baris kosong terakhir).
|
|
134
|
+
let clear = terminal_1.ANSI.clearLine;
|
|
135
|
+
for (let i = 0; i < lines.length; i++)
|
|
136
|
+
clear += terminal_1.ANSI.cursorUp(1) + terminal_1.ANSI.clearLine;
|
|
137
|
+
out.write('\r' + clear);
|
|
138
|
+
out.write(terminal_1.ANSI.showCursor);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function asciiModeFrames() {
|
|
143
|
+
// toAsciiSafe memetakan braille → '*' (3 kolom? tidak — catchall 1 kolom),
|
|
144
|
+
// tapi spinner jadi tidak enak dilihat; varian ASCII asli lebih rapi.
|
|
145
|
+
try {
|
|
146
|
+
// Impur dinamis dihindari; cukup cek env yang sama dengan asciiModeEnabled.
|
|
147
|
+
const v = process.env['VECTOR_ASCII'];
|
|
148
|
+
const on = v !== undefined ? v !== '0' && v.toLowerCase() !== 'false' : false;
|
|
149
|
+
return on ? SPINNER_ASCII : SPINNER_BRAILLE;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return SPINNER_BRAILLE;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function sleep(ms) {
|
|
156
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
157
|
+
}
|
|
158
|
+
function version() {
|
|
159
|
+
try {
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
161
|
+
const pkg = require('../../package.json');
|
|
162
|
+
return pkg.version || '0.0.0';
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return '0.0.0';
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/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