netheriteai-code 0.1.0 → 0.2.3
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/image.png +0 -0
- package/package.json +2 -2
- package/src/ollama.js +35 -44
- package/src/tui.js +57 -33
package/image.png
ADDED
|
Binary file
|
package/package.json
CHANGED
package/src/ollama.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
|
-
const
|
|
5
|
+
const BASE_URL = process.env.NETHERITE_BASE_URL || "http://176.88.249.119:11434";
|
|
6
6
|
const PREFERRED_DEFAULT_MODELS = [
|
|
7
7
|
"glm-5:cloud",
|
|
8
8
|
];
|
|
@@ -18,25 +18,29 @@ function getThinkMode(model) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async function request(pathname, body, signal) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`${BASE_URL}${pathname}`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "content-type": "application/json" },
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
signal,
|
|
27
|
+
});
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error("Server down");
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
return response;
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error("Server down");
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export async function listModels() {
|
|
36
40
|
try {
|
|
37
|
-
const response = await fetch(`${
|
|
41
|
+
const response = await fetch(`${BASE_URL}/api/tags`);
|
|
38
42
|
if (!response.ok) {
|
|
39
|
-
throw new Error(
|
|
43
|
+
throw new Error("Server down");
|
|
40
44
|
}
|
|
41
45
|
const json = await response.json();
|
|
42
46
|
return (json.models || []).map((model) => ({
|
|
@@ -47,41 +51,28 @@ export async function listModels() {
|
|
|
47
51
|
details: model.details || {},
|
|
48
52
|
}));
|
|
49
53
|
} catch {
|
|
50
|
-
|
|
51
|
-
const { stdout } = await execFileAsync("ollama", ["list"], { encoding: "utf8" });
|
|
52
|
-
const lines = stdout.trim().split("\n").slice(1).filter(Boolean);
|
|
53
|
-
return lines.map((line) => {
|
|
54
|
-
const parts = line.trim().split(/\s{2,}/);
|
|
55
|
-
return {
|
|
56
|
-
name: parts[0] || "",
|
|
57
|
-
size: parts[2] || "",
|
|
58
|
-
modifiedAt: parts[3] || "",
|
|
59
|
-
digest: parts[1] || "",
|
|
60
|
-
details: {},
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
} catch {
|
|
64
|
-
throw new Error(
|
|
65
|
-
"Could not connect to NetheriteAI. Start it with `ollama serve`, or set OLLAMA_BASE_URL if it runs elsewhere.",
|
|
66
|
-
);
|
|
67
|
-
}
|
|
54
|
+
throw new Error("Server down");
|
|
68
55
|
}
|
|
69
56
|
}
|
|
70
57
|
|
|
71
58
|
export async function pickDefaultModel() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
59
|
+
try {
|
|
60
|
+
const models = await listModels();
|
|
61
|
+
if (!models.length) {
|
|
62
|
+
throw new Error("Server down");
|
|
63
|
+
}
|
|
64
|
+
const preferredMatch = models.find((model) =>
|
|
65
|
+
PREFERRED_DEFAULT_MODELS.some((preferred) => preferred.toLowerCase() === model.name.toLowerCase()),
|
|
66
|
+
);
|
|
67
|
+
if (preferredMatch) {
|
|
68
|
+
return preferredMatch.name;
|
|
69
|
+
}
|
|
82
70
|
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
const familyMatch = models.find((model) => model.name.toLowerCase().startsWith("netheriteai"));
|
|
72
|
+
return familyMatch?.name || models[0].name;
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error("Server down");
|
|
75
|
+
}
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
export async function chat({ model, messages, tools, signal }) {
|
|
@@ -181,7 +172,7 @@ export async function chatStream({ model, messages, tools, onChunk, onReasoningC
|
|
|
181
172
|
|
|
182
173
|
const reader = response.body?.getReader();
|
|
183
174
|
if (!reader) {
|
|
184
|
-
throw new Error("
|
|
175
|
+
throw new Error("Server down");
|
|
185
176
|
}
|
|
186
177
|
|
|
187
178
|
const decoder = new TextDecoder();
|
package/src/tui.js
CHANGED
|
@@ -2,6 +2,8 @@ const ESC = "\u001b[";
|
|
|
2
2
|
const RESET = "\u001b[0m";
|
|
3
3
|
const BEL = "\u0007";
|
|
4
4
|
|
|
5
|
+
const isWin = process.platform === "win32";
|
|
6
|
+
|
|
5
7
|
const COLORS = {
|
|
6
8
|
fg: "\u001b[38;2;232;232;232m",
|
|
7
9
|
muted: "\u001b[38;2;138;138;138m",
|
|
@@ -17,6 +19,18 @@ const COLORS = {
|
|
|
17
19
|
blackFg: "\u001b[38;2;0;0;0m",
|
|
18
20
|
};
|
|
19
21
|
|
|
22
|
+
const SYMBOLS = {
|
|
23
|
+
badge: isWin ? "[#]" : "▣",
|
|
24
|
+
dot: isWin ? "." : "·",
|
|
25
|
+
ellipsis: "...",
|
|
26
|
+
pointer: isWin ? ">" : "›",
|
|
27
|
+
meta: isWin ? "o" : "◻",
|
|
28
|
+
scroll: isWin ? "#" : "█",
|
|
29
|
+
busyFull: isWin ? "*" : "■",
|
|
30
|
+
busyDim: isWin ? "." : "▪",
|
|
31
|
+
bar: isWin ? "|" : "│",
|
|
32
|
+
};
|
|
33
|
+
|
|
20
34
|
const LOGO_TOP = [
|
|
21
35
|
"NN NN EEEEEEE TTTTTTT H H EEEEEEE RRRRR IIIII TTTTTTT EEEEEEE",
|
|
22
36
|
"NNN NN EE T H H EE RR RR III T EE ",
|
|
@@ -62,7 +76,7 @@ function truncate(text, width) {
|
|
|
62
76
|
const value = String(text ?? "");
|
|
63
77
|
if (width <= 0) return "";
|
|
64
78
|
if (value.length <= width) return value;
|
|
65
|
-
return width
|
|
79
|
+
return width <= 3 ? value.slice(0, width) : `${value.slice(0, width - 3)}${SYMBOLS.ellipsis}`;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
function pad(text, width) {
|
|
@@ -87,9 +101,10 @@ function wrapText(text, width) {
|
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
function paintStatusBadge(frame, row, col, panelColor, agentName) {
|
|
90
|
-
|
|
91
|
-
frame.paintText(row, col
|
|
92
|
-
frame.paintText(row, col +
|
|
104
|
+
const badge = SYMBOLS.badge;
|
|
105
|
+
frame.paintText(row, col, badge, `${panelColor}${COLORS.blue}`);
|
|
106
|
+
frame.paintText(row, col + badge.length + 1, agentName, `${panelColor}${COLORS.fg}`);
|
|
107
|
+
frame.paintText(row, col + badge.length + 1 + agentName.length + 1, SYMBOLS.dot, `${panelColor}${COLORS.dim}`);
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
function displayModelName(model) {
|
|
@@ -394,16 +409,16 @@ function paintBusyIndicator(frame, row, col, now = Date.now()) {
|
|
|
394
409
|
for (let index = 0; index < width; index += 1) {
|
|
395
410
|
const distance = Math.abs(index - head);
|
|
396
411
|
let color = COLORS.dim;
|
|
397
|
-
let char =
|
|
412
|
+
let char = SYMBOLS.dot;
|
|
398
413
|
if (distance < 0.7) {
|
|
399
414
|
color = COLORS.blue;
|
|
400
|
-
char =
|
|
415
|
+
char = SYMBOLS.busyFull;
|
|
401
416
|
} else if (distance < 1.5) {
|
|
402
417
|
color = COLORS.blueSoft;
|
|
403
|
-
char =
|
|
418
|
+
char = SYMBOLS.busyFull;
|
|
404
419
|
} else if (distance < 2.3) {
|
|
405
420
|
color = COLORS.blueFaint;
|
|
406
|
-
char =
|
|
421
|
+
char = SYMBOLS.busyDim;
|
|
407
422
|
}
|
|
408
423
|
frame.paintText(row, col + index, char, `${COLORS.bg}${color}`);
|
|
409
424
|
}
|
|
@@ -437,23 +452,26 @@ function makeFrame(width, height) {
|
|
|
437
452
|
}
|
|
438
453
|
|
|
439
454
|
function flush(cursorRow, cursorCol) {
|
|
440
|
-
const output = [`${ESC}
|
|
455
|
+
const output = [`${ESC}H`];
|
|
441
456
|
for (let r = 0; r < height; r += 1) {
|
|
442
457
|
output.push(move(r + 1, 1));
|
|
443
458
|
let currentStyle = "";
|
|
444
|
-
let
|
|
459
|
+
let lineBuffer = "";
|
|
445
460
|
for (let c = 0; c < width; c += 1) {
|
|
461
|
+
// Safe guard: never draw in the very last cell of the terminal to avoid scrolling
|
|
462
|
+
if (r === height - 1 && c === width - 1) break;
|
|
463
|
+
|
|
446
464
|
const nextStyle = styles[r][c];
|
|
447
465
|
if (nextStyle !== currentStyle) {
|
|
448
|
-
if (
|
|
466
|
+
if (lineBuffer) output.push(lineBuffer);
|
|
449
467
|
output.push(nextStyle);
|
|
450
|
-
|
|
468
|
+
lineBuffer = rows[r][c];
|
|
451
469
|
currentStyle = nextStyle;
|
|
452
470
|
} else {
|
|
453
|
-
|
|
471
|
+
lineBuffer += rows[r][c];
|
|
454
472
|
}
|
|
455
473
|
}
|
|
456
|
-
output.push(
|
|
474
|
+
output.push(lineBuffer, RESET);
|
|
457
475
|
}
|
|
458
476
|
output.push(move(cursorRow, cursorCol));
|
|
459
477
|
process.stdout.write(output.join(""));
|
|
@@ -478,7 +496,8 @@ export function runTui({
|
|
|
478
496
|
}) {
|
|
479
497
|
return new Promise((resolve) => {
|
|
480
498
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
481
|
-
|
|
499
|
+
// Disable wrap and enter alternate buffer
|
|
500
|
+
process.stdout.write("\u001b[?1049h\u001b[?25l\u001b[?1000h\u001b[?1006h\u001b[?7l\u001b[2J");
|
|
482
501
|
setTerminalTitle("NetheriteAI:Code by hurdacu");
|
|
483
502
|
|
|
484
503
|
let mode = "home";
|
|
@@ -507,6 +526,7 @@ export function runTui({
|
|
|
507
526
|
let liveReasoning = "";
|
|
508
527
|
let inputBuffer = "";
|
|
509
528
|
|
|
529
|
+
// Slower render loop for Windows stability (60ms = ~16fps)
|
|
510
530
|
const timer = setInterval(() => {
|
|
511
531
|
const now = Date.now();
|
|
512
532
|
const hasAnimatingTool = transcript.some((item) =>
|
|
@@ -544,7 +564,7 @@ export function runTui({
|
|
|
544
564
|
}
|
|
545
565
|
}
|
|
546
566
|
render();
|
|
547
|
-
},
|
|
567
|
+
}, 60);
|
|
548
568
|
|
|
549
569
|
function addPromptHistory(prompt) {
|
|
550
570
|
if (!prompt.trim()) return;
|
|
@@ -557,7 +577,7 @@ export function runTui({
|
|
|
557
577
|
const text = String(prompt || "").trim().replace(/\s+/g, " ");
|
|
558
578
|
if (!text) return "";
|
|
559
579
|
if (text.length <= maxLength) return text;
|
|
560
|
-
return `${text.slice(0, Math.max(0, maxLength - 3))}
|
|
580
|
+
return `${text.slice(0, Math.max(0, maxLength - 3))}${SYMBOLS.ellipsis}`;
|
|
561
581
|
}
|
|
562
582
|
|
|
563
583
|
function ensureSessionTitle(prompt) {
|
|
@@ -718,7 +738,6 @@ export function runTui({
|
|
|
718
738
|
}
|
|
719
739
|
|
|
720
740
|
function renderHome(frame, width, height) {
|
|
721
|
-
const currentModel = displayModelName(getModel ? getModel() : model);
|
|
722
741
|
const logoWidth = Math.max(LOGO_TOP[0].length, LOGO_BOTTOM[0].length);
|
|
723
742
|
const logoLeft = center(width, logoWidth);
|
|
724
743
|
const logoTop = Math.max(4, Math.floor(height / 2) - 10);
|
|
@@ -738,7 +757,7 @@ export function runTui({
|
|
|
738
757
|
const panelHeight = promptLines.length + 3;
|
|
739
758
|
const panelTop = logoTop + LOGO_TOP.length + LOGO_BOTTOM.length + 3;
|
|
740
759
|
frame.fillRect(panelTop, panelLeft, panelWidth, panelHeight, `${COLORS.panel}${COLORS.fg}`);
|
|
741
|
-
for (let y = 0; y < panelHeight; y += 1) frame.paintText(panelTop + y, panelLeft,
|
|
760
|
+
for (let y = 0; y < panelHeight; y += 1) frame.paintText(panelTop + y, panelLeft, SYMBOLS.bar, `${COLORS.panel}${COLORS.blue}`);
|
|
742
761
|
promptLines.forEach((line, index) => {
|
|
743
762
|
frame.paintText(panelTop + 1 + index, panelLeft + 2, pad(line, panelWidth - 4), `${COLORS.panel}${input ? COLORS.fg : COLORS.dim}`);
|
|
744
763
|
});
|
|
@@ -771,7 +790,7 @@ export function runTui({
|
|
|
771
790
|
frame.paintText(
|
|
772
791
|
top + 2 + i,
|
|
773
792
|
left + 2,
|
|
774
|
-
`${active ?
|
|
793
|
+
`${active ? SYMBOLS.pointer : " "} ${truncate(item.label, boxWidth - 6)}`,
|
|
775
794
|
`${COLORS.panel}${active ? COLORS.blue : COLORS.fg}`,
|
|
776
795
|
);
|
|
777
796
|
}
|
|
@@ -913,7 +932,7 @@ export function runTui({
|
|
|
913
932
|
frame.paintText(1, center(width, `NetheriteAI:Code | ${sessionTitle}`.length), `NetheriteAI:Code | ${sessionTitle}`, `${COLORS.bg}${COLORS.fg}`);
|
|
914
933
|
if (hasSidebar) {
|
|
915
934
|
for (let r = 2; r <= height; r += 1) {
|
|
916
|
-
frame.paintText(r, contentWidth + 3,
|
|
935
|
+
frame.paintText(r, contentWidth + 3, SYMBOLS.bar, `${COLORS.bg}${COLORS.dim}`);
|
|
917
936
|
}
|
|
918
937
|
const sideCol = contentWidth + 6;
|
|
919
938
|
const sideWidth = sidebarWidth - 8;
|
|
@@ -964,20 +983,20 @@ export function runTui({
|
|
|
964
983
|
}
|
|
965
984
|
if (item.kind === "user_top" || item.kind === "user_bottom") {
|
|
966
985
|
frame.fillRect(row, 2, contentWidth - 1, 1, linePanel);
|
|
967
|
-
frame.paintText(row, 2,
|
|
986
|
+
frame.paintText(row, 2, SYMBOLS.bar, borderStyle);
|
|
968
987
|
row += 1;
|
|
969
988
|
continue;
|
|
970
989
|
}
|
|
971
990
|
if (item.kind === "user_line") {
|
|
972
991
|
frame.fillRect(row, 2, contentWidth - 1, 1, linePanel);
|
|
973
|
-
frame.paintText(row, 2,
|
|
992
|
+
frame.paintText(row, 2, SYMBOLS.bar, borderStyle);
|
|
974
993
|
frame.paintText(row, 4, pad(item.text, contentWidth - 5), linePanel);
|
|
975
994
|
row += 1;
|
|
976
995
|
continue;
|
|
977
996
|
}
|
|
978
997
|
if (item.kind === "thinking_line") {
|
|
979
998
|
if (selected) frame.fillRect(row, 3, contentWidth - 3, 1, `${COLORS.bg}${COLORS.fg}`);
|
|
980
|
-
frame.paintText(row, 3,
|
|
999
|
+
frame.paintText(row, 3, SYMBOLS.bar, `${COLORS.bg}${COLORS.dim}`);
|
|
981
1000
|
if (item.first && item.text.startsWith("Thinking:")) {
|
|
982
1001
|
frame.paintText(row, 5, "Thinking:", `${COLORS.bg}${COLORS.amber}`);
|
|
983
1002
|
frame.paintText(row, 15, ` ${item.text.slice(9).trim()}`, `${COLORS.bg}${COLORS.muted}`);
|
|
@@ -1038,14 +1057,14 @@ export function runTui({
|
|
|
1038
1057
|
}
|
|
1039
1058
|
if (item.kind === "meta") {
|
|
1040
1059
|
if (selected) frame.fillRect(row, 3, contentWidth - 3, 1, `${COLORS.bg}${COLORS.fg}`);
|
|
1041
|
-
frame.paintText(row, 4,
|
|
1060
|
+
frame.paintText(row, 4, `${SYMBOLS.meta} ${truncate(item.text, contentWidth - 8)}`, `${COLORS.bg}${selected ? COLORS.fg : COLORS.blue}`);
|
|
1042
1061
|
row += 1;
|
|
1043
1062
|
continue;
|
|
1044
1063
|
}
|
|
1045
1064
|
row += 1;
|
|
1046
1065
|
}
|
|
1047
1066
|
for (let r = 2; r < inputTop; r += 1) {
|
|
1048
|
-
frame.paintText(r, scrollCol,
|
|
1067
|
+
frame.paintText(r, scrollCol, SYMBOLS.bar, `${COLORS.bg}${COLORS.dim}`);
|
|
1049
1068
|
}
|
|
1050
1069
|
if (maxOffset > 0) {
|
|
1051
1070
|
const trackHeight = Math.max(1, inputTop - 2);
|
|
@@ -1053,18 +1072,18 @@ export function runTui({
|
|
|
1053
1072
|
const thumbTravel = Math.max(0, trackHeight - thumbHeight);
|
|
1054
1073
|
const thumbTop = 2 + Math.floor((scrollOffset / maxOffset) * thumbTravel);
|
|
1055
1074
|
for (let r = 0; r < thumbHeight; r += 1) {
|
|
1056
|
-
frame.paintText(thumbTop + r, scrollCol,
|
|
1075
|
+
frame.paintText(thumbTop + r, scrollCol, SYMBOLS.scroll, `${COLORS.bg}${COLORS.muted}`);
|
|
1057
1076
|
}
|
|
1058
1077
|
}
|
|
1059
1078
|
|
|
1060
1079
|
frame.fillRect(inputTop, 2, contentWidth - 1, inputHeight, `${COLORS.panel}${COLORS.fg}`);
|
|
1061
|
-
for (let y = 0; y < inputHeight; y += 1) frame.paintText(inputTop + y, 2,
|
|
1080
|
+
for (let y = 0; y < inputHeight; y += 1) frame.paintText(inputTop + y, 2, SYMBOLS.bar, `${COLORS.panel}${COLORS.blue}`);
|
|
1062
1081
|
paintStatusBadge(frame, inputTop + inputLines.length + 1, 4, COLORS.panel, agentName);
|
|
1063
1082
|
inputLines.forEach((line, index) => {
|
|
1064
1083
|
frame.paintText(inputTop + 1 + index, 4, pad(line, contentWidth - 5), `${COLORS.panel}${COLORS.fg}`);
|
|
1065
1084
|
});
|
|
1066
1085
|
if (cursorVisible) {
|
|
1067
|
-
const cursorLine =
|
|
1086
|
+
const cursorLine = promptLines.length - 1;
|
|
1068
1087
|
const cursorCol = 4 + inputLines[cursorLine].length;
|
|
1069
1088
|
paintInputCursor(frame, inputTop + 1 + cursorLine, cursorCol);
|
|
1070
1089
|
}
|
|
@@ -1275,7 +1294,8 @@ export function runTui({
|
|
|
1275
1294
|
process.stdin.removeListener("data", onData);
|
|
1276
1295
|
process.stdout.removeListener("resize", render);
|
|
1277
1296
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1278
|
-
|
|
1297
|
+
// Restore wrapping and exit alternate buffer
|
|
1298
|
+
process.stdout.write("\u001b[?7h\u001b[?1000l\u001b[?1006l\u001b[?25h\u001b[?1049l");
|
|
1279
1299
|
resolve({ reason, transcript: [...transcript] });
|
|
1280
1300
|
}
|
|
1281
1301
|
|
|
@@ -1325,9 +1345,13 @@ export function runTui({
|
|
|
1325
1345
|
activity = "Command palette not wired yet";
|
|
1326
1346
|
return render();
|
|
1327
1347
|
}
|
|
1328
|
-
if (key.name === "escape"
|
|
1329
|
-
|
|
1330
|
-
|
|
1348
|
+
if (key.name === "escape") {
|
|
1349
|
+
if (busy) {
|
|
1350
|
+
activity = "Interrupt requested";
|
|
1351
|
+
currentAbortController?.abort();
|
|
1352
|
+
} else {
|
|
1353
|
+
input = "";
|
|
1354
|
+
}
|
|
1331
1355
|
return render();
|
|
1332
1356
|
}
|
|
1333
1357
|
if (key.name === "pageup") {
|