bonecode 1.2.1 → 1.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/compat/opencode_adapter.ts +60 -21
- package/dist/compat/opencode_adapter.js +54 -15
- package/dist/compat/opencode_adapter.js.map +1 -1
- package/dist/src/cli.js +63 -30
- package/dist/src/cli.js.map +1 -1
- package/dist/src/engine/session/prompt.js +9 -0
- package/dist/src/engine/session/prompt.js.map +1 -1
- package/dist/src/engine/session/tool_registry.js +28 -2
- package/dist/src/engine/session/tool_registry.js.map +1 -1
- package/dist/src/stats.d.ts +3 -0
- package/dist/src/stats.js +43 -3
- package/dist/src/stats.js.map +1 -1
- package/dist/src/tui.js +118 -29
- package/dist/src/tui.js.map +1 -1
- package/package.json +1 -1
- package/scripts/test_tui_render.js +278 -0
- package/scripts/test_v1_2.js +420 -0
- package/src/cli.ts +67 -33
- package/src/engine/session/prompt.ts +10 -0
- package/src/engine/session/tool_registry.ts +33 -2
- package/src/stats.ts +55 -3
- package/src/tui.ts +115 -30
package/src/stats.ts
CHANGED
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Token usage and cost statistics
|
|
3
3
|
* Ported from opencode's cli/cmd/stats.ts — adapted for BoneCode's Postgres schema.
|
|
4
|
+
*
|
|
5
|
+
* Works on both Postgres (full schema with token columns on sessions) and
|
|
6
|
+
* SQLite fallback (sessions has only id/title/etc; token data lives on messages).
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { Pool } from "pg"
|
|
7
10
|
|
|
11
|
+
interface CostColumns {
|
|
12
|
+
cost: boolean
|
|
13
|
+
tokensInput: boolean
|
|
14
|
+
tokensOutput: boolean
|
|
15
|
+
tokensReasoning: boolean
|
|
16
|
+
tokensCacheRead: boolean
|
|
17
|
+
tokensCacheWrite: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let _columnsCache: CostColumns | null = null
|
|
21
|
+
|
|
22
|
+
async function detectSessionCostColumns(pool: Pool): Promise<CostColumns> {
|
|
23
|
+
if (_columnsCache) return _columnsCache
|
|
24
|
+
|
|
25
|
+
const probe = async (col: string): Promise<boolean> => {
|
|
26
|
+
try {
|
|
27
|
+
// SELECT col FROM sessions LIMIT 0 — fails if column doesn't exist, succeeds otherwise.
|
|
28
|
+
// Works identically on Postgres and SQLite.
|
|
29
|
+
await pool.query(`SELECT ${col} FROM sessions LIMIT 0`)
|
|
30
|
+
return true
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_columnsCache = {
|
|
37
|
+
cost: await probe("cost_usd"),
|
|
38
|
+
tokensInput: await probe("tokens_input"),
|
|
39
|
+
tokensOutput: await probe("tokens_output"),
|
|
40
|
+
tokensReasoning: await probe("tokens_reasoning"),
|
|
41
|
+
tokensCacheRead: await probe("tokens_cache_read"),
|
|
42
|
+
tokensCacheWrite: await probe("tokens_cache_write"),
|
|
43
|
+
}
|
|
44
|
+
return _columnsCache
|
|
45
|
+
}
|
|
46
|
+
|
|
8
47
|
export interface SessionStats {
|
|
9
48
|
totalSessions: number
|
|
10
49
|
totalMessages: number
|
|
@@ -50,10 +89,23 @@ export async function aggregateStats(
|
|
|
50
89
|
return opts.days
|
|
51
90
|
})()
|
|
52
91
|
|
|
92
|
+
// Detect available columns by introspection — works on both Postgres and SQLite
|
|
93
|
+
const hasCol = await detectSessionCostColumns(pool)
|
|
94
|
+
const sessionSelect = [
|
|
95
|
+
"id",
|
|
96
|
+
"project_id",
|
|
97
|
+
"created_at",
|
|
98
|
+
"updated_at",
|
|
99
|
+
hasCol.cost ? "cost_usd" : "0 AS cost_usd",
|
|
100
|
+
hasCol.tokensInput ? "tokens_input" : "0 AS tokens_input",
|
|
101
|
+
hasCol.tokensOutput ? "tokens_output" : "0 AS tokens_output",
|
|
102
|
+
hasCol.tokensReasoning ? "tokens_reasoning" : "0 AS tokens_reasoning",
|
|
103
|
+
hasCol.tokensCacheRead ? "tokens_cache_read" : "0 AS tokens_cache_read",
|
|
104
|
+
hasCol.tokensCacheWrite ? "tokens_cache_write" : "0 AS tokens_cache_write",
|
|
105
|
+
].join(", ")
|
|
106
|
+
|
|
53
107
|
// Load sessions
|
|
54
|
-
let sessionQuery = `SELECT
|
|
55
|
-
tokens_cache_read, tokens_cache_write, created_at, updated_at
|
|
56
|
-
FROM sessions WHERE state != 'deleted'`
|
|
108
|
+
let sessionQuery = `SELECT ${sessionSelect} FROM sessions WHERE state != 'deleted'`
|
|
57
109
|
const params: unknown[] = []
|
|
58
110
|
let idx = 1
|
|
59
111
|
|
package/src/tui.ts
CHANGED
|
@@ -393,19 +393,78 @@ interface ToolTrack {
|
|
|
393
393
|
* "text" — pass deltas through verbatim (with partial-fence detection)
|
|
394
394
|
* "fence" — buffer everything; emit marker on close fence
|
|
395
395
|
*/
|
|
396
|
+
/**
|
|
397
|
+
* Apply lightweight inline markdown styling to a streamed chunk.
|
|
398
|
+
*
|
|
399
|
+
* - `# `, `## `, `### ` at line-start become bold colored headers
|
|
400
|
+
* - `**text**` becomes bold
|
|
401
|
+
* - `` `code` `` becomes inverted gray
|
|
402
|
+
* - `- ` and `* ` at line-start become a bullet
|
|
403
|
+
*
|
|
404
|
+
* Operates per-line on the chunk. Cross-chunk markdown (a `**` opening in one
|
|
405
|
+
* delta and closing in another) is not handled — we just leave those raw.
|
|
406
|
+
* That's acceptable because it's rare and the model's output is still readable.
|
|
407
|
+
*/
|
|
408
|
+
function applyInlineMarkdown(text: string): string {
|
|
409
|
+
if (!text) return text;
|
|
410
|
+
// Split on newlines so we can match line-anchored patterns (headers, bullets)
|
|
411
|
+
const lines = text.split("\n");
|
|
412
|
+
const styled = lines.map((line) => {
|
|
413
|
+
// Headers
|
|
414
|
+
let m = line.match(/^(#{1,6})\s+(.*)$/);
|
|
415
|
+
if (m) {
|
|
416
|
+
const level = m[1].length;
|
|
417
|
+
const content = m[2];
|
|
418
|
+
if (level === 1) return `${CYAN}${BOLD}${content}${R}`;
|
|
419
|
+
if (level === 2) return `${CYAN}${BOLD}${content}${R}`;
|
|
420
|
+
if (level === 3) return `${WHITE}${BOLD}${content}${R}`;
|
|
421
|
+
return `${WHITE}${content}${R}`;
|
|
422
|
+
}
|
|
423
|
+
// Bullets at line start
|
|
424
|
+
m = line.match(/^(\s*)[-*]\s+(.*)$/);
|
|
425
|
+
if (m) {
|
|
426
|
+
line = `${m[1]}${GRAY}•${R} ${m[2]}`;
|
|
427
|
+
}
|
|
428
|
+
// Numbered lists
|
|
429
|
+
m = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
430
|
+
if (m) {
|
|
431
|
+
line = `${m[1]}${GRAY}${m[2]}${R} ${m[3]}`;
|
|
432
|
+
}
|
|
433
|
+
// Bold (**text**)
|
|
434
|
+
line = line.replace(/\*\*([^*]+)\*\*/g, `${BOLD}$1${R}`);
|
|
435
|
+
// Inline code (`code`)
|
|
436
|
+
line = line.replace(/`([^`]+)`/g, `${GRAY}$1${R}`);
|
|
437
|
+
return line;
|
|
438
|
+
});
|
|
439
|
+
return styled.join("\n");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Stateful code-fence collapser.
|
|
444
|
+
*
|
|
445
|
+
* Replaces ```lang ... ``` blocks with a one-line marker
|
|
446
|
+
* " ┃ code: lang, N lines" so streaming code blocks don't flood the TUI.
|
|
447
|
+
*
|
|
448
|
+
* State:
|
|
449
|
+
* "text" — pass through verbatim (with partial-fence buffering)
|
|
450
|
+
* "fence" — silently buffer body; emit final marker on close
|
|
451
|
+
*
|
|
452
|
+
* Newline handling:
|
|
453
|
+
* We strip up to one trailing newline before the opening fence and one
|
|
454
|
+
* leading newline after the closing fence so the marker sits on its own
|
|
455
|
+
* line without doubling blank lines around it.
|
|
456
|
+
*/
|
|
396
457
|
function makeCodeFenceCollapser() {
|
|
397
458
|
let mode: "text" | "fence" = "text";
|
|
398
459
|
let lang = "";
|
|
399
|
-
let buffered = "";
|
|
400
|
-
let pending = "";
|
|
401
|
-
|
|
402
|
-
const lines = (s: string) => s.split("\n").length;
|
|
460
|
+
let buffered = "";
|
|
461
|
+
let pending = "";
|
|
403
462
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return
|
|
408
|
-
}
|
|
463
|
+
const countLines = (s: string) => {
|
|
464
|
+
if (!s) return 0;
|
|
465
|
+
const trimmed = s.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
466
|
+
return trimmed ? trimmed.split("\n").length : 0;
|
|
467
|
+
};
|
|
409
468
|
|
|
410
469
|
return {
|
|
411
470
|
feed(chunk: string): string {
|
|
@@ -416,23 +475,26 @@ function makeCodeFenceCollapser() {
|
|
|
416
475
|
|
|
417
476
|
while (i < buf.length) {
|
|
418
477
|
if (mode === "text") {
|
|
419
|
-
// Look for ``` to enter fence mode
|
|
420
478
|
const fenceIdx = buf.indexOf("```", i);
|
|
421
479
|
if (fenceIdx === -1) {
|
|
422
|
-
// No fence in remaining text
|
|
423
|
-
//
|
|
480
|
+
// No fence in remaining text. Hold last 2 chars in case they're
|
|
481
|
+
// the start of an upcoming fence.
|
|
424
482
|
const safeEnd = Math.max(i, buf.length - 2);
|
|
425
483
|
result += buf.slice(i, safeEnd);
|
|
426
484
|
pending = buf.slice(safeEnd);
|
|
427
485
|
i = buf.length;
|
|
428
486
|
break;
|
|
429
487
|
}
|
|
430
|
-
// Emit text up to the fence
|
|
431
|
-
|
|
432
|
-
|
|
488
|
+
// Emit text up to the fence, but trim a single trailing newline so
|
|
489
|
+
// the marker sits on its own line without extra blank space.
|
|
490
|
+
let preFence = buf.slice(i, fenceIdx);
|
|
491
|
+
if (preFence.endsWith("\n")) preFence = preFence.slice(0, -1);
|
|
492
|
+
result += preFence;
|
|
493
|
+
|
|
494
|
+
// Read the language declaration (everything up to the next newline).
|
|
433
495
|
const nlIdx = buf.indexOf("\n", fenceIdx + 3);
|
|
434
496
|
if (nlIdx === -1) {
|
|
435
|
-
//
|
|
497
|
+
// Opening line not complete yet — buffer for next feed.
|
|
436
498
|
pending = buf.slice(fenceIdx);
|
|
437
499
|
i = buf.length;
|
|
438
500
|
break;
|
|
@@ -441,31 +503,36 @@ function makeCodeFenceCollapser() {
|
|
|
441
503
|
i = nlIdx + 1;
|
|
442
504
|
mode = "fence";
|
|
443
505
|
buffered = "";
|
|
444
|
-
//
|
|
445
|
-
|
|
506
|
+
// Don't emit anything yet — the final marker is written when we
|
|
507
|
+
// see the closing fence so we have the line count.
|
|
446
508
|
continue;
|
|
447
509
|
}
|
|
448
510
|
|
|
449
511
|
// mode === "fence" — look for closing ```
|
|
450
512
|
const closeIdx = buf.indexOf("```", i);
|
|
451
513
|
if (closeIdx === -1) {
|
|
514
|
+
// No close fence yet — buffer the body.
|
|
452
515
|
buffered += buf.slice(i);
|
|
453
|
-
// Hold last 2 chars in case they're part of an upcoming close fence
|
|
516
|
+
// Hold last 2 chars in case they're part of an upcoming close fence.
|
|
454
517
|
const safeEnd = Math.max(i, buf.length - 2);
|
|
455
518
|
buffered = buffered.slice(0, buffered.length - (buf.length - safeEnd));
|
|
456
519
|
pending = buf.slice(safeEnd);
|
|
457
520
|
i = buf.length;
|
|
458
521
|
break;
|
|
459
522
|
}
|
|
460
|
-
|
|
523
|
+
|
|
524
|
+
// Closing fence found.
|
|
461
525
|
buffered += buf.slice(i, closeIdx);
|
|
462
|
-
const lineCount =
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
result += `\
|
|
526
|
+
const lineCount = countLines(buffered);
|
|
527
|
+
// Emit the marker on its own line with leading and trailing newlines.
|
|
528
|
+
// The outer indenter will prepend " " to each line break.
|
|
529
|
+
result += `\n${GRAY}┃ code: ${lang}, ${lineCount} line${lineCount === 1 ? "" : "s"}${R}`;
|
|
466
530
|
i = closeIdx + 3;
|
|
467
|
-
// Skip
|
|
531
|
+
// Skip a single newline immediately after the close fence to avoid
|
|
532
|
+
// doubling blank lines.
|
|
468
533
|
if (buf[i] === "\n") i++;
|
|
534
|
+
// Add a trailing newline so the next text starts on a fresh line.
|
|
535
|
+
result += "\n";
|
|
469
536
|
mode = "text";
|
|
470
537
|
lang = "";
|
|
471
538
|
buffered = "";
|
|
@@ -474,12 +541,12 @@ function makeCodeFenceCollapser() {
|
|
|
474
541
|
return result;
|
|
475
542
|
},
|
|
476
543
|
flush(): string {
|
|
477
|
-
const tail =
|
|
544
|
+
const tail = pending;
|
|
545
|
+
pending = "";
|
|
478
546
|
if (mode === "fence") {
|
|
479
|
-
|
|
480
|
-
const lineCount = lines(buffered.replace(/\n+$/, ""));
|
|
547
|
+
const lineCount = countLines(buffered);
|
|
481
548
|
mode = "text";
|
|
482
|
-
return tail + `\
|
|
549
|
+
return tail + `\n${GRAY}┃ code: ${lang}, ${lineCount}+ lines${R}\n`;
|
|
483
550
|
}
|
|
484
551
|
return tail;
|
|
485
552
|
},
|
|
@@ -563,8 +630,10 @@ async function streamPrompt(opts: {
|
|
|
563
630
|
// code blocks appear as "[code: lang]" placeholders instead of
|
|
564
631
|
// dumping raw source.
|
|
565
632
|
const piece = collapseCodeFences(text);
|
|
633
|
+
// Apply lightweight markdown styling (headers, bold, inline code)
|
|
634
|
+
const styled = applyInlineMarkdown(piece);
|
|
566
635
|
// Print with leading-newline indenting (so each new line gets the 3-space prefix)
|
|
567
|
-
const indented =
|
|
636
|
+
const indented = styled.replace(/\n/g, `\n `);
|
|
568
637
|
out(indented);
|
|
569
638
|
fullText += text;
|
|
570
639
|
continue;
|
|
@@ -727,6 +796,22 @@ export async function runTUI(opts: {
|
|
|
727
796
|
port: number; token: string; model: string;
|
|
728
797
|
provider: string; worktree: string; sessionId?: string;
|
|
729
798
|
}): Promise<void> {
|
|
799
|
+
// Force UTF-8 output on Windows so emoji and box-drawing chars render
|
|
800
|
+
// correctly. Without this, console code page 437/850 corrupts multi-byte
|
|
801
|
+
// sequences into "??" or mojibake.
|
|
802
|
+
if (process.platform === "win32") {
|
|
803
|
+
try {
|
|
804
|
+
// Set output encoding for stdout/stderr
|
|
805
|
+
(process.stdout as any).setEncoding?.("utf-8");
|
|
806
|
+
(process.stderr as any).setEncoding?.("utf-8");
|
|
807
|
+
// Switch the console to UTF-8 (code page 65001)
|
|
808
|
+
const { execSync } = require("child_process");
|
|
809
|
+
execSync("chcp 65001", { stdio: "ignore" });
|
|
810
|
+
} catch {
|
|
811
|
+
// chcp not available — that's fine, we tried
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
730
815
|
let { model, provider } = opts;
|
|
731
816
|
const { port, token, worktree } = opts;
|
|
732
817
|
let sessionId = opts.sessionId || null;
|