goatchain 0.0.1

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/cli/ui.mjs ADDED
@@ -0,0 +1,138 @@
1
+ export function formatOneLine(text, maxLen) {
2
+ const s = String(text ?? '').replace(/\s+/g, ' ').trim()
3
+ if (s.length <= maxLen)
4
+ return s
5
+ return `${s.slice(0, Math.max(0, maxLen - 1))}…`
6
+ }
7
+
8
+ function supportsAnsi() {
9
+ if (!process.stdout.isTTY)
10
+ return false
11
+ if (process.env.TERM === 'dumb')
12
+ return false
13
+ if ('NO_COLOR' in process.env)
14
+ return false
15
+ return true
16
+ }
17
+
18
+ function wrapAnsi(text, ...opens) {
19
+ if (!supportsAnsi())
20
+ return String(text ?? '')
21
+ return `${opens.join('')}${String(text ?? '')}\u001B[0m`
22
+ }
23
+
24
+ export const ansi = {
25
+ bold: text => wrapAnsi(text, '\u001B[1m'),
26
+ dim: text => wrapAnsi(text, '\u001B[2m'),
27
+ gray: text => wrapAnsi(text, '\u001B[90m'),
28
+ red: text => wrapAnsi(text, '\u001B[31m'),
29
+ green: text => wrapAnsi(text, '\u001B[32m'),
30
+ yellow: text => wrapAnsi(text, '\u001B[33m'),
31
+ cyan: text => wrapAnsi(text, '\u001B[36m'),
32
+ inverse: text => wrapAnsi(text, '\u001B[7m'),
33
+ header: text => wrapAnsi(text, '\u001B[1m', '\u001B[7m'),
34
+ highlight: text => wrapAnsi(text, '\u001B[30m', '\u001B[43m'),
35
+ }
36
+
37
+ export function messageToTextPreview(message) {
38
+ if (!message || typeof message !== 'object')
39
+ return ''
40
+ const c = message.content
41
+ if (typeof c === 'string')
42
+ return c
43
+ try {
44
+ return JSON.stringify(c)
45
+ }
46
+ catch {
47
+ return String(c)
48
+ }
49
+ }
50
+
51
+ export function deriveSessionTitle(messages) {
52
+ const firstUser = Array.isArray(messages)
53
+ ? messages.find(m => m && typeof m === 'object' && m.role === 'user' && formatOneLine(messageToTextPreview(m), 999).length > 0)
54
+ : undefined
55
+ const txt = firstUser ? messageToTextPreview(firstUser) : ''
56
+ return formatOneLine(txt || 'New session', 60) || 'New session'
57
+ }
58
+
59
+ export function deriveSessionSummary(messages) {
60
+ const arr = Array.isArray(messages) ? messages : []
61
+ const firstUser = arr.find(m => m && typeof m === 'object' && m.role === 'user')
62
+ const lastUser = [...arr].reverse().find(m => m && typeof m === 'object' && m.role === 'user')
63
+ const lastAssistant = [...arr].reverse().find(m => m && typeof m === 'object' && m.role === 'assistant')
64
+ const a = formatOneLine(firstUser ? messageToTextPreview(firstUser) : '', 80)
65
+ const b = formatOneLine(lastUser ? messageToTextPreview(lastUser) : '', 80)
66
+ const c = formatOneLine(lastAssistant ? messageToTextPreview(lastAssistant) : '', 80)
67
+ const parts = [a ? `first: ${a}` : '', b && b !== a ? `last user: ${b}` : '', c ? `last assistant: ${c}` : ''].filter(Boolean)
68
+ return formatOneLine(parts.join(' | '), 160)
69
+ }
70
+
71
+ export function formatWhen(ts) {
72
+ if (typeof ts !== 'number' || !Number.isFinite(ts))
73
+ return ''
74
+ try {
75
+ const d = new Date(ts)
76
+ const pad = n => String(n).padStart(2, '0')
77
+ const yyyy = d.getFullYear()
78
+ const mm = pad(d.getMonth() + 1)
79
+ const dd = pad(d.getDate())
80
+ const hh = pad(d.getHours())
81
+ const min = pad(d.getMinutes())
82
+ const now = new Date()
83
+ const includeYear = yyyy !== now.getFullYear()
84
+ return includeYear ? `${yyyy}-${mm}-${dd} ${hh}:${min}` : `${mm}-${dd} ${hh}:${min}`
85
+ }
86
+ catch {
87
+ return ''
88
+ }
89
+ }
90
+
91
+ export function printHistory(messages, options = {}) {
92
+ const arr = Array.isArray(messages) ? messages : []
93
+ const maxMessages = typeof options.maxMessages === 'number' ? options.maxMessages : 12
94
+ const maxCharsPerMessage = typeof options.maxCharsPerMessage === 'number' ? options.maxCharsPerMessage : 600
95
+ const useEmoji = Boolean(options.emoji) && process.stdout.isTTY && process.env.TERM !== 'dumb'
96
+
97
+ const filtered = arr.filter(m => m && typeof m === 'object' && m.role !== 'system')
98
+ if (filtered.length === 0) {
99
+ process.stdout.write('[no history]\n')
100
+ return
101
+ }
102
+
103
+ const start = Math.max(0, filtered.length - maxMessages)
104
+ const slice = filtered.slice(start)
105
+ const omitted = start
106
+
107
+ if (omitted > 0)
108
+ process.stdout.write(ansi.dim(`[... ${omitted} earlier messages omitted ...]\n`))
109
+
110
+ for (const m of slice) {
111
+ const role = typeof m.role === 'string' ? m.role : 'unknown'
112
+ const label = role === 'assistant' ? 'assistant' : role === 'user' ? 'user' : role
113
+ const icon = useEmoji
114
+ ? label === 'assistant'
115
+ ? '🤖'
116
+ : label === 'user'
117
+ ? '👤'
118
+ : label === 'tool'
119
+ ? '🛠️'
120
+ : '📌'
121
+ : ''
122
+ const labelText = useEmoji ? `${icon} ${label}` : label
123
+ const labelStyled = label === 'assistant'
124
+ ? ansi.cyan(labelText)
125
+ : label === 'user'
126
+ ? ansi.green(labelText)
127
+ : ansi.gray(label)
128
+ const text = formatOneLine(messageToTextPreview(m), maxCharsPerMessage)
129
+ process.stdout.write(`[${labelStyled}] ${text}\n`)
130
+ }
131
+ }
132
+
133
+ export function clearScreen() {
134
+ if (!process.stdout.isTTY)
135
+ return
136
+ // Clear screen + move cursor to home.
137
+ process.stdout.write('\u001B[2J\u001B[H')
138
+ }
package/cli.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli/index.mjs'
3
+
4
+ await main(process.argv.slice(2))
5
+