novacode 0.5.5 → 0.7.0

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.
@@ -1,113 +0,0 @@
1
- import { Box, Text } from "ink"
2
- import type { Msg } from "../../types.ts"
3
- import { formatToolArgs } from "../../util.ts"
4
- import { formatMarkdown } from "../markdown.ts"
5
-
6
- const TOOL_STYLE: Record<string, string> = {
7
- read: "blue",
8
- write: "magenta",
9
- edit: "yellow",
10
- bash: "cyan",
11
- glob: "green",
12
- find: "green",
13
- grep: "green",
14
- tree: "green",
15
- }
16
-
17
- export function hasMeaningfulContent(msg: Msg): boolean {
18
- if (msg.role === "user") return true
19
- if (msg.role === "tool_result") return true
20
- if (msg.role === "assistant") {
21
- if (msg.model === "system") return true
22
- return msg.content.some((c) => {
23
- if (c.type === "thinking") return c.text.trim().length > 0
24
- if (c.type === "text") return c.text.trim().length > 0
25
- return false
26
- })
27
- }
28
- return false
29
- }
30
-
31
- export function Message({ msg, isFirst }: { msg: Msg; isFirst: boolean }) {
32
- if (msg.role === "user") {
33
- return (
34
- <Box marginTop={isFirst ? 0 : 1} flexDirection="row">
35
- <Box flexShrink={0} marginRight={1}>
36
- <Text bold color="green">
37
- {">"}
38
- </Text>
39
- </Box>
40
- <Box flexGrow={1} flexShrink={1}>
41
- <Text>
42
- {typeof msg.content === "string"
43
- ? msg.content
44
- : msg.content.map((c) => (c.type === "text" ? c.text : "")).join("")}
45
- </Text>
46
- </Box>
47
- </Box>
48
- )
49
- }
50
-
51
- if (msg.role === "assistant") {
52
- if (msg.model === "system") {
53
- return (
54
- <Box flexDirection="column" marginTop={0}>
55
- {msg.content.map((c, i) =>
56
- // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
57
- c.type === "text" ? <Text key={i}>{formatMarkdown(c.text)}</Text> : null,
58
- )}
59
- </Box>
60
- )
61
- }
62
-
63
- const hasVisibleContent = msg.content.some((c) => c.type === "text" || c.type === "thinking")
64
- if (!hasVisibleContent) return null
65
-
66
- return (
67
- <Box flexDirection="column" marginTop={0}>
68
- {msg.content.map((c, i) => {
69
- if (c.type === "thinking") {
70
- return (
71
- // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
72
- <Text key={i} dimColor italic>
73
- {c.text}
74
- </Text>
75
- )
76
- }
77
- if (c.type === "text") {
78
- // biome-ignore lint/suspicious/noArrayIndexKey: stable turn content
79
- return <Text key={i}>{formatMarkdown(c.text)}</Text>
80
- }
81
- return null
82
- })}
83
- </Box>
84
- )
85
- }
86
-
87
- if (msg.role === "tool_result") {
88
- const args = msg.args ? formatToolArgs(msg.args, true) : ""
89
-
90
- const resText = msg.content
91
- .map((c) => (c.type === "text" ? c.text : ""))
92
- .join("")
93
- .trim()
94
-
95
- const isRead = msg.tool === "read"
96
- const lineCount = isRead ? resText.split("\n").length : 0
97
- const color = TOOL_STYLE[msg.tool] || "white"
98
-
99
- return (
100
- <Box flexDirection="row">
101
- <Text color={msg.isError ? "red" : "green"}>{msg.isError ? "✗" : "✓"} </Text>
102
- <Text color={color} bold>
103
- {msg.tool}
104
- </Text>
105
- {args && <Text> {args}</Text>}
106
- {isRead && !msg.isError && <Text dimColor> ({lineCount} lines)</Text>}
107
- {msg.isError && resText && <Text color="red"> {resText.slice(0, 80)}</Text>}
108
- </Box>
109
- )
110
- }
111
-
112
- return null
113
- }
@@ -1,58 +0,0 @@
1
- import { Box, Text } from "ink"
2
- import type { Model } from "../../types.ts"
3
-
4
- function fmtK(n: number): string {
5
- const k = n / 1000
6
- return k >= 100 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`
7
- }
8
-
9
- function formatTokenUsage(used: number, contextWindow: number): string {
10
- if (used === 0) return `0/${fmtK(contextWindow)}`
11
- const pct = Math.round((used / contextWindow) * 100)
12
- return `${fmtK(used)}/${fmtK(contextWindow)} (${pct}%)`
13
- }
14
-
15
- export function StatusBar({
16
- model,
17
- usage,
18
- busy,
19
- suggestions,
20
- selCmdIdx,
21
- }: {
22
- model: Model
23
- usage: { in: number; out: number }
24
- busy: boolean
25
- suggestions: Array<{ name: string; desc: string }>
26
- selCmdIdx: number
27
- }) {
28
- return (
29
- <Box justifyContent="space-between">
30
- <Box>
31
- {suggestions.length > 0 ? (
32
- <Box flexDirection="column" marginLeft={2}>
33
- {suggestions.map((s, i) => (
34
- <Box key={s.name}>
35
- <Text
36
- color={i === selCmdIdx ? "black" : "yellow"}
37
- backgroundColor={i === selCmdIdx ? "yellow" : undefined}
38
- >
39
- /{s.name.padEnd(10)}
40
- </Text>
41
- <Text dimColor> {s.desc}</Text>
42
- </Box>
43
- ))}
44
- </Box>
45
- ) : (
46
- <Text dimColor>Enter to send · /help for commands</Text>
47
- )}
48
- </Box>
49
-
50
- <Box>
51
- <Text dimColor>{formatTokenUsage(usage.in, model.contextWindow)}</Text>
52
- <Text dimColor> │ </Text>
53
- <Text dimColor>{model.id}</Text>
54
- {busy && <Text dimColor> │ Esc to stop</Text>}
55
- </Box>
56
- </Box>
57
- )
58
- }
@@ -1,62 +0,0 @@
1
- import chalk from "chalk"
2
-
3
- export class MarkdownRenderer {
4
- #inCodeBlock = false
5
- #codeBlockLang = ""
6
-
7
- renderLine(line: string): string {
8
- if (line.startsWith("```")) {
9
- if (this.#inCodeBlock) {
10
- this.#inCodeBlock = false
11
- return chalk.dim(`└${"─".repeat(50)}`)
12
- }
13
- this.#inCodeBlock = true
14
- this.#codeBlockLang = line.slice(3).trim()
15
- return chalk.dim(
16
- "┌" +
17
- "─".repeat(10) +
18
- ` [Code: ${this.#codeBlockLang || "text"}] ` +
19
- "─".repeat(40 - (this.#codeBlockLang?.length || 4)),
20
- )
21
- }
22
-
23
- if (this.#inCodeBlock) {
24
- return chalk.cyan(`│ ${line}`)
25
- }
26
-
27
- if (line.startsWith("#")) {
28
- const match = line.match(/^(#{1,6})\s+(.*)$/)
29
- if (match?.[1] && match[2]) {
30
- const level = match[1].length
31
- const content = match[2]
32
- if (level === 1) return chalk.bold.magenta.underline(content)
33
- if (level === 2) return chalk.bold.blue(content)
34
- return chalk.bold.cyan(content)
35
- }
36
- }
37
-
38
- let formatted = line
39
- if (formatted.startsWith("- ") || formatted.startsWith("* ")) {
40
- formatted = ` ${chalk.yellow("•")} ${formatted.slice(2)}`
41
- }
42
-
43
- formatted = formatted.replace(/`([^`]+)`/g, (_, code) => chalk.yellow(code))
44
- formatted = formatted.replace(/\*\*([^*]+)\*\*/g, (_, bold) => chalk.bold(bold))
45
- formatted = formatted.replace(/__([^_]+)__/g, (_, bold) => chalk.bold(bold))
46
- formatted = formatted.replace(/\*([^*]+)\*/g, (_, italic) => chalk.italic(italic))
47
- formatted = formatted.replace(/_([^_]+)_/g, (_, italic) => chalk.italic(italic))
48
- formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
49
- return `${chalk.blue(text)} ${chalk.dim(`(${url})`)}`
50
- })
51
-
52
- return formatted
53
- }
54
- }
55
-
56
- export function formatMarkdown(text: string): string {
57
- const renderer = new MarkdownRenderer()
58
- return text
59
- .split("\n")
60
- .map((line) => renderer.renderLine(line))
61
- .join("\n")
62
- }
@@ -1,205 +0,0 @@
1
- import { Box, render, Text, useInput } from "ink"
2
- import { useState } from "react"
3
-
4
- interface SelectOption {
5
- value: string
6
- label: string
7
- hint?: string
8
- }
9
-
10
- export function SelectPrompt({
11
- message,
12
- options,
13
- header,
14
- onSelect,
15
- }: {
16
- message: string
17
- options: SelectOption[]
18
- header?: string
19
- onSelect: (value: string | null) => void
20
- }) {
21
- const [idx, setIdx] = useState(0)
22
-
23
- useInput((_, key) => {
24
- if (key.escape) {
25
- onSelect(null)
26
- return
27
- }
28
- if (key.upArrow) {
29
- setIdx((i) => (i - 1 + options.length) % options.length)
30
- return
31
- }
32
- if (key.downArrow) {
33
- setIdx((i) => (i + 1) % options.length)
34
- return
35
- }
36
- if (key.return) {
37
- onSelect(options[idx]?.value ?? null)
38
- }
39
- })
40
-
41
- return (
42
- <Box flexDirection="column" paddingX={1}>
43
- {header && (
44
- <Box marginBottom={1}>
45
- <Text>{header}</Text>
46
- </Box>
47
- )}
48
- <Box marginBottom={1}>
49
- <Text bold>{message}</Text>
50
- </Box>
51
- {options.map((opt, i) => (
52
- <Box key={opt.value}>
53
- <Text color={i === idx ? "green" : undefined}>
54
- {i === idx ? "❯ " : " "}
55
- {opt.label}
56
- </Text>
57
- {opt.hint && i === idx && <Text dimColor> {opt.hint}</Text>}
58
- </Box>
59
- ))}
60
- <Box marginTop={1}>
61
- <Text dimColor>↑↓ navigate · Enter select · Esc cancel</Text>
62
- </Box>
63
- </Box>
64
- )
65
- }
66
-
67
- export function PasswordPrompt({
68
- message,
69
- validate,
70
- onSubmit,
71
- }: {
72
- message: string
73
- validate?: (v: string) => string | undefined
74
- onSubmit: (value: string | null) => void
75
- }) {
76
- const [value, setValue] = useState("")
77
- const [error, setError] = useState("")
78
-
79
- useInput((ch, key) => {
80
- if (key.escape) {
81
- onSubmit(null)
82
- return
83
- }
84
- if (key.return) {
85
- const err = validate?.(value)
86
- if (err) {
87
- setError(err)
88
- return
89
- }
90
- onSubmit(value)
91
- return
92
- }
93
- if (key.backspace || key.delete) {
94
- setValue((v) => v.slice(0, -1))
95
- setError("")
96
- return
97
- }
98
- if (ch) {
99
- setValue((v) => v + ch)
100
- setError("")
101
- }
102
- })
103
-
104
- return (
105
- <Box flexDirection="column" paddingX={1}>
106
- <Box marginBottom={1}>
107
- <Text bold>{message}</Text>
108
- </Box>
109
- <Box>
110
- <Text color="green">│ </Text>
111
- <Text dimColor>{"*".repeat(value.length)}</Text>
112
- <Text color="green">│</Text>
113
- </Box>
114
- {error && (
115
- <Box>
116
- <Text color="red">{error}</Text>
117
- </Box>
118
- )}
119
- <Box marginTop={1}>
120
- <Text dimColor>Enter submit · Esc cancel</Text>
121
- </Box>
122
- </Box>
123
- )
124
- }
125
-
126
- export function ConfirmPrompt({
127
- message,
128
- onConfirm,
129
- }: {
130
- message: string
131
- onConfirm: (value: boolean | null) => void
132
- }) {
133
- const [yes, setYes] = useState(true)
134
-
135
- useInput((_, key) => {
136
- if (key.escape) {
137
- onConfirm(null)
138
- return
139
- }
140
- if (key.leftArrow || key.rightArrow || key.tab) {
141
- setYes((y) => !y)
142
- return
143
- }
144
- if (key.return) {
145
- onConfirm(yes)
146
- }
147
- })
148
-
149
- return (
150
- <Box flexDirection="column" paddingX={1}>
151
- <Box marginBottom={1}>
152
- <Text bold>{message}</Text>
153
- </Box>
154
- <Box>
155
- <Text color={yes ? "green" : undefined}>{yes ? "❯ " : " "}Yes</Text>
156
- </Box>
157
- <Box>
158
- <Text color={!yes ? "red" : undefined}>{!yes ? "❯ " : " "}No</Text>
159
- </Box>
160
- <Box marginTop={1}>
161
- <Text dimColor>←→ toggle · Enter confirm · Esc cancel</Text>
162
- </Box>
163
- </Box>
164
- )
165
- }
166
-
167
- // Standalone wrappers for use outside the main TUI (e.g. onboarding)
168
-
169
- export function standaloneSelect(
170
- message: string,
171
- options: SelectOption[],
172
- header?: string,
173
- ): Promise<string | null> {
174
- return new Promise((resolve) => {
175
- const { unmount } = render(
176
- <SelectPrompt
177
- message={message}
178
- options={options}
179
- header={header}
180
- onSelect={(v) => {
181
- unmount()
182
- resolve(v)
183
- }}
184
- />,
185
- )
186
- })
187
- }
188
-
189
- export function standalonePassword(
190
- message: string,
191
- validate?: (v: string) => string | undefined,
192
- ): Promise<string | null> {
193
- return new Promise((resolve) => {
194
- const { unmount } = render(
195
- <PasswordPrompt
196
- message={message}
197
- validate={validate}
198
- onSubmit={(v) => {
199
- unmount()
200
- resolve(v)
201
- }}
202
- />,
203
- )
204
- })
205
- }
package/src/types.ts DELETED
@@ -1,248 +0,0 @@
1
- /**
2
- * Shared type definitions for the entire project.
3
- * Includes messaging, tools, providers, and agent loop events.
4
- */
5
- /** Content Parts */
6
-
7
- export interface TextPart {
8
- type: "text"
9
- text: string
10
- signature?: string
11
- }
12
-
13
- export interface ImagePart {
14
- type: "image"
15
- data: string // base64
16
- mime: string
17
- }
18
-
19
- export interface ThinkPart {
20
- type: "thinking"
21
- text: string
22
- signature?: string
23
- }
24
-
25
- export interface ToolCallPart {
26
- type: "tool_call"
27
- id: string
28
- name: string
29
- args: Record<string, unknown>
30
- signature?: string
31
- }
32
-
33
- export type ContentPart = TextPart | ImagePart | ThinkPart | ToolCallPart
34
-
35
- /** Messages */
36
-
37
- export interface UserMsg {
38
- role: "user"
39
- content: string | ContentPart[]
40
- ts: number
41
- }
42
-
43
- export interface AssistantMsg {
44
- role: "assistant"
45
- content: ContentPart[]
46
- model: string
47
- provider: string
48
- usage: Usage
49
- stop: StopReason
50
- error?: string
51
- ts: number
52
- }
53
-
54
- export interface ToolResultMsg {
55
- role: "tool_result"
56
- callId: string
57
- tool: string
58
- args?: Record<string, unknown>
59
- content: ContentPart[]
60
- isError: boolean
61
- ts: number
62
- }
63
-
64
- export type Msg = UserMsg | AssistantMsg | ToolResultMsg
65
- export type StopReason = "stop" | "length" | "tool_use" | "error" | "aborted"
66
-
67
- /** Usage */
68
-
69
- export interface Usage {
70
- in: number
71
- out: number
72
- cacheRead?: number
73
- cacheWrite?: number
74
- }
75
-
76
- /** Provider */
77
-
78
- export type ApiFormat = "openai" | "gemini"
79
-
80
- export interface ProviderDef {
81
- id: string
82
- name: string
83
- api: ApiFormat
84
- baseUrl: string
85
- envKey: string // env var name for API key
86
- }
87
-
88
- export interface Model {
89
- id: string
90
- name: string
91
- provider: string
92
- contextWindow: number
93
- maxTokens: number
94
- supportsThinking: boolean
95
- }
96
-
97
- /** Tools */
98
-
99
- export interface ToolDef {
100
- name: string
101
- description: string
102
- parameters: ToolParamDef
103
- }
104
-
105
- export interface ToolParamDef {
106
- type: "object"
107
- properties: Record<string, ToolPropDef>
108
- required?: string[]
109
- }
110
-
111
- export interface ToolPropDef {
112
- type: string
113
- description?: string
114
- enum?: string[]
115
- items?: ToolPropDef
116
- properties?: Record<string, ToolPropDef>
117
- required?: string[]
118
- }
119
-
120
- export interface ToolResult {
121
- content: ContentPart[]
122
- isError: boolean
123
- }
124
-
125
- export type ToolExecuteFn = (
126
- args: Record<string, unknown>,
127
- signal?: AbortSignal,
128
- ) => Promise<ToolResult>
129
-
130
- export interface Tool {
131
- def: ToolDef
132
- execute: ToolExecuteFn
133
- }
134
-
135
- /** Agent Events */
136
-
137
- export type AgentEvent =
138
- | { type: "start" }
139
- | { type: "turn" }
140
- | { type: "text_delta"; text: string }
141
- | { type: "thinking_delta"; text: string }
142
- | { type: "tool_call"; call: ToolCallPart }
143
- | { type: "assistant_msg"; msg: AssistantMsg }
144
- | { type: "tool_result"; callId: string; result: ToolResultMsg; args?: Record<string, unknown> }
145
- | { type: "turn_end"; msg: AssistantMsg; results: ToolResultMsg[] }
146
- | { type: "usage"; usage: Usage }
147
-
148
- /** Config */
149
-
150
- export interface NovaConfig {
151
- provider: string
152
- model: string
153
- }
154
-
155
- export interface NovaAuth {
156
- apiKeys: Record<string, string> // provider -> key
157
- }
158
-
159
- /** Session */
160
-
161
- export interface Session {
162
- id: string
163
- cwd: string
164
- model: string
165
- provider: string
166
- title: string | null
167
- created: number
168
- updated: number
169
- }
170
-
171
- /** Loop & Provider Types */
172
-
173
- export interface LoopCtx {
174
- system: string
175
- messages: Msg[]
176
- tools: Tool[]
177
- }
178
-
179
- export interface LoopOpts {
180
- api: ApiFormat
181
- model: Model
182
- apiKey: string
183
- baseUrl: string
184
- maxTurns?: number
185
- // Intercept tool calls before they execute
186
- beforeTool?: (
187
- call: ToolCallPart,
188
- args: Record<string, unknown>,
189
- ctx: LoopCtx,
190
- ) => Promise<{ block?: boolean; reason?: string } | undefined>
191
- // Run logic after a tool completes
192
- afterTool?: (call: ToolCallPart, result: ToolResultMsg, ctx: LoopCtx) => Promise<void>
193
- }
194
-
195
- export interface StreamOpts {
196
- api: ApiFormat
197
- model: Model
198
- apiKey: string
199
- baseUrl: string
200
- system: string
201
- messages: Msg[]
202
- tools: ToolDef[]
203
- signal?: AbortSignal
204
- }
205
-
206
- export interface IEventStream<T, R> {
207
- [Symbol.asyncIterator](): AsyncGenerator<T>
208
- result: R | undefined
209
- isDone: boolean
210
- }
211
-
212
- export type StreamFn = (opts: StreamOpts) => IEventStream<StreamEvent, AssistantResult>
213
-
214
- export interface StreamEvent {
215
- type: "text_delta" | "thinking_delta" | "tool_call" | "usage"
216
- text?: string
217
- call?: ToolCallPart
218
- usage?: Usage
219
- }
220
-
221
- export interface AssistantResult {
222
- content: ContentPart[]
223
- usage: Usage
224
- stop: StopReason
225
- }
226
-
227
- /** Prompts — used by interactive commands within the TUI */
228
-
229
- export interface Prompts {
230
- select(config: {
231
- message: string
232
- header?: string
233
- options: Array<{ value: string; label: string; hint?: string }>
234
- }): Promise<string | null>
235
- password(config: {
236
- message: string
237
- validate?: (v: string) => string | undefined
238
- }): Promise<string | null>
239
- confirm(config: { message: string }): Promise<boolean | null>
240
- }
241
-
242
- /** Commands */
243
-
244
- export interface Cmd {
245
- name: string
246
- desc: string
247
- aliases?: string[]
248
- }