@iletai/nzb 1.7.0 → 1.7.4

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.
@@ -0,0 +1,59 @@
1
+ import { C, LABEL_PAD, NZB_LABEL } from "./ansi.js";
2
+ /** Render a single line of markdown to ANSI (used by both streaming and batch). */
3
+ export function renderLine(line, inCodeBlock) {
4
+ if (inCodeBlock) {
5
+ return ` ${C.dim("│")} ${line}`;
6
+ }
7
+ if (/^[-*_]{3,}\s*$/.test(line))
8
+ return C.dim("──────────────────────────────────");
9
+ if (line.startsWith("### "))
10
+ return C.coral(line.slice(4));
11
+ if (line.startsWith("## "))
12
+ return C.boldWhite(line.slice(3));
13
+ if (line.startsWith("# "))
14
+ return C.boldWhite(line.slice(2));
15
+ if (line.startsWith("> "))
16
+ return `${C.dim("│")} ${C.dim(line.slice(2))}`;
17
+ if (/^ {2,}[-*] /.test(line))
18
+ return ` ◦ ${line.replace(/^ +[-*] /, "")}`;
19
+ if (/^[-*] /.test(line))
20
+ return ` • ${line.slice(2)}`;
21
+ if (/^\d+\. /.test(line))
22
+ return ` ${line}`;
23
+ return line;
24
+ }
25
+ /** Apply inline formatting (bold, code, links, etc.) to already-rendered text. */
26
+ export function applyInlineFormatting(text) {
27
+ return text
28
+ .replace(/\*\*\*(.+?)\*\*\*/g, `\x1b[1;3m$1\x1b[0m`)
29
+ .replace(/\*\*(.+?)\*\*/g, `\x1b[1m$1\x1b[0m`)
30
+ .replace(/~~(.+?)~~/g, `\x1b[9m$1\x1b[0m`)
31
+ .replace(/`([^`]+)`/g, C.yellow("$1"))
32
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => `${t} ${C.dim(`(${u})`)}`);
33
+ }
34
+ /** Render a complete markdown document to ANSI (used for proactive/background messages). */
35
+ export function renderMarkdown(text) {
36
+ let inCodeBlock = false;
37
+ const rendered = text.split("\n").map((line) => {
38
+ if (/^```/.test(line)) {
39
+ if (inCodeBlock) {
40
+ inCodeBlock = false;
41
+ return "";
42
+ }
43
+ inCodeBlock = true;
44
+ const lang = line.slice(3).trim();
45
+ return lang ? C.dim(lang) : "";
46
+ }
47
+ return renderLine(line, inCodeBlock);
48
+ });
49
+ return applyInlineFormatting(rendered.join("\n"));
50
+ }
51
+ /** Write a rendered message with a role label (NZB/SYS). */
52
+ export function writeLabeled(role, text) {
53
+ const label = role === "nzb" ? NZB_LABEL : ` ${C.dim("SYS")} `;
54
+ const lines = text.split("\n");
55
+ for (let i = 0; i < lines.length; i++) {
56
+ process.stdout.write((i === 0 ? label : LABEL_PAD) + lines[i] + "\n");
57
+ }
58
+ }
59
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1,163 @@
1
+ import { C, NZB_LABEL, LABEL_PAD } from "./ansi.js";
2
+ import { debugLog, previewForDebug } from "./debug.js";
3
+ import { renderLine, applyInlineFormatting } from "./renderer.js";
4
+ // Shared request context — updated by index.ts before each request
5
+ export const streamContext = {
6
+ requestId: 0,
7
+ requestStartedAt: 0,
8
+ };
9
+ // ── Stream buffer state ──────────────────────────────────
10
+ let streamLineBuffer = "";
11
+ let inStreamCodeBlock = false;
12
+ let streamIsFirstLine = true;
13
+ /** Get the prefix for the current stream line (label or padding). */
14
+ function streamPrefix() {
15
+ return streamIsFirstLine ? NZB_LABEL : LABEL_PAD;
16
+ }
17
+ function stripLeadingStreamNewlines(text) {
18
+ if (!streamIsFirstLine || streamLineBuffer.length > 0)
19
+ return text;
20
+ const stripped = text.replace(/^(?:\r?\n)+/, "");
21
+ if (stripped.length !== text.length) {
22
+ debugLog("stream-strip-leading-newlines", {
23
+ requestId: streamContext.requestId,
24
+ removedChars: text.length - stripped.length,
25
+ originalPreview: previewForDebug(text),
26
+ });
27
+ }
28
+ return stripped;
29
+ }
30
+ /** Clear the current visual line (handles terminal wrapping). */
31
+ function clearVisualLine(charCount) {
32
+ const cols = process.stdout.columns || 80;
33
+ const up = Math.ceil(Math.max(charCount, 1) / cols) - 1;
34
+ debugLog("clear-visual-line", { requestId: streamContext.requestId, charCount, cols, up });
35
+ if (up > 0)
36
+ process.stdout.write(`\x1b[${up}A`);
37
+ process.stdout.write(`\r\x1b[J`);
38
+ }
39
+ /** Render a buffered line and write it with the appropriate prefix. */
40
+ function writeRenderedStreamLine(line) {
41
+ const prefix = streamPrefix();
42
+ if (/^```/.test(line)) {
43
+ if (inStreamCodeBlock) {
44
+ inStreamCodeBlock = false;
45
+ }
46
+ else {
47
+ inStreamCodeBlock = true;
48
+ const lang = line.slice(3).trim();
49
+ process.stdout.write(prefix + (lang ? C.dim(lang) : ""));
50
+ }
51
+ }
52
+ else {
53
+ const rendered = applyInlineFormatting(renderLine(line, inStreamCodeBlock));
54
+ process.stdout.write(prefix + rendered);
55
+ }
56
+ process.stdout.write("\n");
57
+ streamIsFirstLine = false;
58
+ }
59
+ /** Process a chunk of streaming text, rendering complete lines with labels. */
60
+ export function writeStreamChunk(newText) {
61
+ debugLog("stream-chunk", {
62
+ requestId: streamContext.requestId,
63
+ length: newText.length,
64
+ preview: previewForDebug(newText),
65
+ startsWithNewline: /^(?:\r?\n)/.test(newText),
66
+ });
67
+ let pos = 0;
68
+ while (pos < newText.length) {
69
+ const nl = newText.indexOf("\n", pos);
70
+ if (nl === -1) {
71
+ // No newline — buffer and write raw with prefix if at line start
72
+ const partial = newText.slice(pos);
73
+ if (streamLineBuffer.length === 0) {
74
+ process.stdout.write(streamPrefix());
75
+ }
76
+ streamLineBuffer += partial;
77
+ process.stdout.write(partial);
78
+ return;
79
+ }
80
+ // Got a complete line
81
+ const segment = newText.slice(pos, nl);
82
+ const hadPartial = streamLineBuffer.length > 0;
83
+ streamLineBuffer += segment;
84
+ if (hadPartial) {
85
+ // Clear the partially-written raw text
86
+ clearVisualLine(10 + streamLineBuffer.length);
87
+ }
88
+ if (streamLineBuffer.length === 0 && !hadPartial) {
89
+ // Empty line
90
+ process.stdout.write(streamPrefix() + "\n");
91
+ streamIsFirstLine = false;
92
+ }
93
+ else {
94
+ writeRenderedStreamLine(streamLineBuffer);
95
+ }
96
+ streamLineBuffer = "";
97
+ pos = nl + 1;
98
+ }
99
+ }
100
+ /** Normalize streaming text by stripping leading newlines from the first chunk. */
101
+ export function normalizeStreamText(text) {
102
+ return stripLeadingStreamNewlines(text);
103
+ }
104
+ /** Flush any remaining partial line and reset streaming state. */
105
+ export function flushStreamState() {
106
+ if (streamLineBuffer.length > 0) {
107
+ clearVisualLine(10 + streamLineBuffer.length);
108
+ writeRenderedStreamLine(streamLineBuffer);
109
+ }
110
+ streamLineBuffer = "";
111
+ inStreamCodeBlock = false;
112
+ streamIsFirstLine = true;
113
+ }
114
+ /** Reset stream buffer state without flushing content. */
115
+ export function resetStreamState() {
116
+ streamLineBuffer = "";
117
+ inStreamCodeBlock = false;
118
+ streamIsFirstLine = true;
119
+ }
120
+ // ── Thinking indicator ────────────────────────────────────
121
+ let thinkingTimer;
122
+ let thinkingFrame = 0;
123
+ let thinkingVisible = false;
124
+ const thinkingFrames = ["Thinking", "Thinking.", "Thinking..", "Thinking..."];
125
+ export function startThinking() {
126
+ stopThinking("restart-thinking");
127
+ thinkingFrame = 0;
128
+ thinkingVisible = true;
129
+ process.stdout.write(`\n${NZB_LABEL}${C.dim(thinkingFrames[0])}`);
130
+ debugLog("thinking-start", {
131
+ requestId: streamContext.requestId,
132
+ frame: thinkingFrames[0],
133
+ msSinceSubmit: streamContext.requestStartedAt > 0 ? Date.now() - streamContext.requestStartedAt : null,
134
+ });
135
+ thinkingTimer = setInterval(() => {
136
+ thinkingFrame = (thinkingFrame + 1) % thinkingFrames.length;
137
+ process.stdout.write(`\r\x1b[K${NZB_LABEL}${C.dim(thinkingFrames[thinkingFrame])}`);
138
+ debugLog("thinking-tick", {
139
+ requestId: streamContext.requestId,
140
+ frameIndex: thinkingFrame,
141
+ frame: thinkingFrames[thinkingFrame],
142
+ });
143
+ }, 400);
144
+ }
145
+ export function stopThinking(reason = "unspecified") {
146
+ const hadTimer = Boolean(thinkingTimer);
147
+ const wasVisible = thinkingVisible;
148
+ if (thinkingTimer) {
149
+ clearInterval(thinkingTimer);
150
+ thinkingTimer = undefined;
151
+ }
152
+ if (thinkingVisible) {
153
+ process.stdout.write(`\r\x1b[K`);
154
+ thinkingVisible = false;
155
+ }
156
+ debugLog("thinking-stop", {
157
+ requestId: streamContext.requestId,
158
+ reason,
159
+ hadTimer,
160
+ wasVisible,
161
+ });
162
+ }
163
+ //# sourceMappingURL=stream.js.map
package/dist/update.js CHANGED
@@ -10,6 +10,7 @@ function getPackageJson() {
10
10
  return { name: pkg.name || PKG_NAME, version: pkg.version || "0.0.0" };
11
11
  }
12
12
  catch {
13
+ // Expected: package.json may not be found in dev/bundled environments
13
14
  return { name: PKG_NAME, version: "0.0.0" };
14
15
  }
15
16
  }
@@ -34,6 +35,7 @@ export async function getLatestVersion() {
34
35
  return result || null;
35
36
  }
36
37
  catch {
38
+ // Expected: npm registry may be unreachable
37
39
  return null;
38
40
  }
39
41
  }
package/dist/utils.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Shared utility functions for NZB.
3
+ */
4
+ /** Error thrown when a promise exceeds its timeout. */
5
+ export class TimeoutError extends Error {
6
+ constructor(ms, label) {
7
+ const msg = label
8
+ ? `Operation "${label}" timed out after ${ms}ms`
9
+ : `Operation timed out after ${ms}ms`;
10
+ super(msg);
11
+ this.name = "TimeoutError";
12
+ }
13
+ }
14
+ /**
15
+ * Wraps a promise with a timeout. Rejects with `TimeoutError` if the
16
+ * promise doesn't settle within `ms` milliseconds.
17
+ */
18
+ export function withTimeout(promise, ms, label) {
19
+ return new Promise((resolve, reject) => {
20
+ const timer = setTimeout(() => {
21
+ reject(new TimeoutError(ms, label));
22
+ }, ms);
23
+ promise.then((value) => {
24
+ clearTimeout(timer);
25
+ resolve(value);
26
+ }, (err) => {
27
+ clearTimeout(timer);
28
+ reject(err);
29
+ });
30
+ });
31
+ }
32
+ /**
33
+ * Returns a human-readable age string from a timestamp.
34
+ * Examples: "5s", "2m 30s", "1h 5m", "3d 2h"
35
+ */
36
+ export function formatAge(startedAt) {
37
+ const totalSeconds = Math.floor((Date.now() - startedAt) / 1000);
38
+ if (totalSeconds < 0)
39
+ return "0s";
40
+ if (totalSeconds < 60)
41
+ return `${totalSeconds}s`;
42
+ const days = Math.floor(totalSeconds / 86400);
43
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
44
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
45
+ const seconds = totalSeconds % 60;
46
+ if (days > 0)
47
+ return `${days}d ${hours}h`;
48
+ if (hours > 0)
49
+ return `${hours}h ${minutes}m`;
50
+ return `${minutes}m ${seconds}s`;
51
+ }
52
+ /**
53
+ * Unicode-safe text truncation. Appends "…" if the text was truncated.
54
+ * Uses `Array.from` to avoid splitting multi-byte characters or emoji.
55
+ */
56
+ export function truncateText(text, maxLength) {
57
+ if (maxLength <= 0)
58
+ return "";
59
+ if (text.length === 0)
60
+ return "";
61
+ const chars = Array.from(text);
62
+ if (chars.length <= maxLength)
63
+ return text;
64
+ return chars.slice(0, maxLength).join("") + "…";
65
+ }
66
+ /**
67
+ * Simple async mutex. Callers `acquire()` the lock and receive a `release`
68
+ * function. If the lock is held, callers queue in FIFO order.
69
+ */
70
+ export function asyncLock() {
71
+ let locked = false;
72
+ const queue = [];
73
+ function acquire() {
74
+ return new Promise((resolve) => {
75
+ const run = () => {
76
+ locked = true;
77
+ resolve(release);
78
+ };
79
+ if (!locked) {
80
+ run();
81
+ }
82
+ else {
83
+ queue.push(run);
84
+ }
85
+ });
86
+ }
87
+ function release() {
88
+ const next = queue.shift();
89
+ if (next) {
90
+ next();
91
+ }
92
+ else {
93
+ locked = false;
94
+ }
95
+ }
96
+ return { acquire };
97
+ }
98
+ /** Returns a promise that resolves after `ms` milliseconds. */
99
+ export function sleep(ms) {
100
+ return new Promise((resolve) => setTimeout(resolve, ms));
101
+ }
102
+ //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.7.0",
3
+ "version": "1.7.4",
4
4
  "description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "nzb": "dist/cli.js"