novacode 0.6.0 → 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.
- package/dist/app-CbJSUNmf.mjs +22 -0
- package/dist/app-CbJSUNmf.mjs.map +1 -0
- package/dist/main.mjs +42 -29
- package/dist/main.mjs.map +1 -1
- package/package.json +1 -2
- package/dist/app-bQ9a_p_K.mjs +0 -22
- package/dist/app-bQ9a_p_K.mjs.map +0 -1
- package/src/agent/agent.ts +0 -87
- package/src/agent/loop.ts +0 -237
- package/src/agent/prompt.ts +0 -50
- package/src/commands/compact.ts +0 -28
- package/src/commands/index.ts +0 -128
- package/src/commands/models.ts +0 -85
- package/src/commands/providers.ts +0 -213
- package/src/commands/session.ts +0 -52
- package/src/config/providers.ts +0 -207
- package/src/config/store.ts +0 -66
- package/src/main.ts +0 -205
- package/src/onboarding/wizard.ts +0 -54
- package/src/provider/gemini.ts +0 -269
- package/src/provider/openai.ts +0 -239
- package/src/provider/stream.ts +0 -138
- package/src/session/compact.ts +0 -159
- package/src/session/store.ts +0 -209
- package/src/tools/fs.ts +0 -189
- package/src/tools/git.ts +0 -99
- package/src/tools/index.ts +0 -33
- package/src/tools/search.ts +0 -274
- package/src/tools/shell.ts +0 -90
- package/src/tools/web.ts +0 -239
- package/src/tui/app.tsx +0 -454
- package/src/tui/components/liveArea.tsx +0 -70
- package/src/tui/components/message.tsx +0 -117
- package/src/tui/components/statusBar.tsx +0 -64
- package/src/tui/constants.ts +0 -25
- package/src/tui/markdown.ts +0 -62
- package/src/tui/prompts.tsx +0 -205
- package/src/types.ts +0 -262
- package/src/update.ts +0 -89
- package/src/util.ts +0 -80
package/src/tui/constants.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI-specific static constants and configuration styles.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
6
|
-
|
|
7
|
-
export const TOOL_STYLE: Record<string, string> = {
|
|
8
|
-
read: "blue",
|
|
9
|
-
write: "magenta",
|
|
10
|
-
edit: "yellow",
|
|
11
|
-
bash: "cyan",
|
|
12
|
-
glob: "green",
|
|
13
|
-
find: "green",
|
|
14
|
-
grep: "green",
|
|
15
|
-
tree: "green",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const TERMINATION_PHRASES = [
|
|
19
|
-
"Terminated by user",
|
|
20
|
-
"Aborted by user",
|
|
21
|
-
"Execution stopped",
|
|
22
|
-
"Interrupted by user",
|
|
23
|
-
"Agent halted",
|
|
24
|
-
"Stopped by user",
|
|
25
|
-
]
|
package/src/tui/markdown.ts
DELETED
|
@@ -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
|
-
}
|
package/src/tui/prompts.tsx
DELETED
|
@@ -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,262 +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
|
-
export interface Compaction {
|
|
172
|
-
summary: string
|
|
173
|
-
seqBefore: number
|
|
174
|
-
filesRead: string[]
|
|
175
|
-
filesWrote: string[]
|
|
176
|
-
ts: number
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export interface CompactResult {
|
|
180
|
-
compacted: boolean
|
|
181
|
-
summary?: string
|
|
182
|
-
msgsRemoved: number
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Loop & Provider Types */
|
|
186
|
-
|
|
187
|
-
export interface LoopCtx {
|
|
188
|
-
system: string
|
|
189
|
-
messages: Msg[]
|
|
190
|
-
tools: Tool[]
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface LoopOpts {
|
|
194
|
-
api: ApiFormat
|
|
195
|
-
model: Model
|
|
196
|
-
apiKey: string
|
|
197
|
-
baseUrl: string
|
|
198
|
-
maxTurns?: number
|
|
199
|
-
// Intercept tool calls before they execute
|
|
200
|
-
beforeTool?: (
|
|
201
|
-
call: ToolCallPart,
|
|
202
|
-
args: Record<string, unknown>,
|
|
203
|
-
ctx: LoopCtx,
|
|
204
|
-
) => Promise<{ block?: boolean; reason?: string } | undefined>
|
|
205
|
-
// Run logic after a tool completes
|
|
206
|
-
afterTool?: (call: ToolCallPart, result: ToolResultMsg, ctx: LoopCtx) => Promise<void>
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export interface StreamOpts {
|
|
210
|
-
api: ApiFormat
|
|
211
|
-
model: Model
|
|
212
|
-
apiKey: string
|
|
213
|
-
baseUrl: string
|
|
214
|
-
system: string
|
|
215
|
-
messages: Msg[]
|
|
216
|
-
tools: ToolDef[]
|
|
217
|
-
signal?: AbortSignal
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface IEventStream<T, R> {
|
|
221
|
-
[Symbol.asyncIterator](): AsyncGenerator<T>
|
|
222
|
-
result: R | undefined
|
|
223
|
-
isDone: boolean
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export type StreamFn = (opts: StreamOpts) => IEventStream<StreamEvent, AssistantResult>
|
|
227
|
-
|
|
228
|
-
export interface StreamEvent {
|
|
229
|
-
type: "text_delta" | "thinking_delta" | "tool_call" | "usage"
|
|
230
|
-
text?: string
|
|
231
|
-
call?: ToolCallPart
|
|
232
|
-
usage?: Usage
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export interface AssistantResult {
|
|
236
|
-
content: ContentPart[]
|
|
237
|
-
usage: Usage
|
|
238
|
-
stop: StopReason
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Prompts — used by interactive commands within the TUI */
|
|
242
|
-
|
|
243
|
-
export interface Prompts {
|
|
244
|
-
select(config: {
|
|
245
|
-
message: string
|
|
246
|
-
header?: string
|
|
247
|
-
options: Array<{ value: string; label: string; hint?: string }>
|
|
248
|
-
}): Promise<string | null>
|
|
249
|
-
password(config: {
|
|
250
|
-
message: string
|
|
251
|
-
validate?: (v: string) => string | undefined
|
|
252
|
-
}): Promise<string | null>
|
|
253
|
-
confirm(config: { message: string }): Promise<boolean | null>
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Commands */
|
|
257
|
-
|
|
258
|
-
export interface Cmd {
|
|
259
|
-
name: string
|
|
260
|
-
desc: string
|
|
261
|
-
aliases?: string[]
|
|
262
|
-
}
|
package/src/update.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process"
|
|
2
|
-
import { readFile } from "node:fs/promises"
|
|
3
|
-
import { dirname, join } from "node:path"
|
|
4
|
-
import { fileURLToPath } from "node:url"
|
|
5
|
-
import semver from "semver"
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
|
|
9
|
-
let cachedLatest: string | null = null
|
|
10
|
-
let cachedCurrent: string | null = null
|
|
11
|
-
|
|
12
|
-
export async function getCurrentVersion(): Promise<string> {
|
|
13
|
-
if (cachedCurrent) return cachedCurrent
|
|
14
|
-
try {
|
|
15
|
-
const raw = await readFile(join(__dirname, "..", "package.json"), "utf-8")
|
|
16
|
-
const pkg = JSON.parse(raw)
|
|
17
|
-
cachedCurrent = (pkg.version as string) ?? "0.0.0"
|
|
18
|
-
return cachedCurrent
|
|
19
|
-
} catch {
|
|
20
|
-
return "0.0.0"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function getLatestVersion(): Promise<string | null> {
|
|
25
|
-
if (cachedLatest) return cachedLatest
|
|
26
|
-
try {
|
|
27
|
-
const proc = spawn("npm", ["info", "novacode", "version"], {
|
|
28
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
29
|
-
})
|
|
30
|
-
const text = await new Promise<string>((resolve, reject) => {
|
|
31
|
-
let out = ""
|
|
32
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
33
|
-
out += chunk.toString()
|
|
34
|
-
})
|
|
35
|
-
proc.on("error", reject)
|
|
36
|
-
proc.on("close", () => resolve(out.trim()))
|
|
37
|
-
})
|
|
38
|
-
if (text) {
|
|
39
|
-
cachedLatest = text
|
|
40
|
-
return text
|
|
41
|
-
}
|
|
42
|
-
} catch {}
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function checkForUpdate(): Promise<{
|
|
47
|
-
hasUpdate: boolean
|
|
48
|
-
current: string
|
|
49
|
-
latest: string
|
|
50
|
-
} | null> {
|
|
51
|
-
const current = await getCurrentVersion()
|
|
52
|
-
const latest = await getLatestVersion()
|
|
53
|
-
if (!latest) return null
|
|
54
|
-
return {
|
|
55
|
-
hasUpdate: semver.gt(latest, current),
|
|
56
|
-
current,
|
|
57
|
-
latest,
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function runUpdate(silent = false): Promise<boolean> {
|
|
62
|
-
try {
|
|
63
|
-
const proc = spawn("npm", ["update", "-g", "novacode"], {
|
|
64
|
-
stdio: silent ? "ignore" : "inherit",
|
|
65
|
-
})
|
|
66
|
-
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
67
|
-
proc.on("error", reject)
|
|
68
|
-
proc.on("close", (code) => resolve(code ?? -1))
|
|
69
|
-
})
|
|
70
|
-
if (exitCode === 0) {
|
|
71
|
-
if (!silent) {
|
|
72
|
-
console.log("✓ novacode updated to latest version successfully.")
|
|
73
|
-
}
|
|
74
|
-
return true
|
|
75
|
-
} else {
|
|
76
|
-
if (!silent) {
|
|
77
|
-
console.error(`Update failed (exit code ${exitCode})`)
|
|
78
|
-
process.exit(1)
|
|
79
|
-
}
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
} catch (e) {
|
|
83
|
-
if (!silent) {
|
|
84
|
-
console.error(`Update failed: ${(e as Error).message}`)
|
|
85
|
-
process.exit(1)
|
|
86
|
-
}
|
|
87
|
-
return false
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/util.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { isAbsolute, relative } from "node:path"
|
|
2
|
-
import chalk from "chalk"
|
|
3
|
-
import type { Msg, TextPart } from "./types.ts"
|
|
4
|
-
|
|
5
|
-
// ~4 chars per token for English/code. Close enough for capacity warnings.
|
|
6
|
-
export function estimateTokens(messages: Msg[]): number {
|
|
7
|
-
let chars = 0
|
|
8
|
-
for (const msg of messages) {
|
|
9
|
-
if (typeof msg.content === "string") {
|
|
10
|
-
chars += msg.content.length
|
|
11
|
-
} else if (Array.isArray(msg.content)) {
|
|
12
|
-
for (const part of msg.content) {
|
|
13
|
-
if (part.type === "text") chars += part.text.length
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return Math.ceil(chars / 4)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function textPart(s: string): TextPart {
|
|
21
|
-
return { type: "text", text: s }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getRelativeIfInside(cwd: string, filePath: string): string {
|
|
25
|
-
if (filePath === cwd || filePath.startsWith(`${cwd}/`)) {
|
|
26
|
-
return relative(cwd, filePath) || "."
|
|
27
|
-
}
|
|
28
|
-
return filePath
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function makeRelative(val: string): string {
|
|
32
|
-
if (typeof val !== "string") return val
|
|
33
|
-
|
|
34
|
-
let pathVal = val
|
|
35
|
-
let prefix = ""
|
|
36
|
-
if (val.startsWith("file://")) {
|
|
37
|
-
pathVal = val.slice(7)
|
|
38
|
-
prefix = "file://"
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (isAbsolute(pathVal)) {
|
|
42
|
-
const cwd = process.cwd()
|
|
43
|
-
return prefix + getRelativeIfInside(cwd, pathVal)
|
|
44
|
-
}
|
|
45
|
-
return val
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function formatToolArgs(
|
|
49
|
-
args: Record<string, unknown> | undefined,
|
|
50
|
-
useChalk = false,
|
|
51
|
-
): string {
|
|
52
|
-
if (!args) return ""
|
|
53
|
-
return Object.entries(args)
|
|
54
|
-
.map(([k, v]) => {
|
|
55
|
-
const val = typeof v === "string" ? makeRelative(v) : JSON.stringify(v)
|
|
56
|
-
const valStr = val.length > 40 ? `${val.slice(0, 40)}…` : val
|
|
57
|
-
const keyStr = useChalk ? chalk.dim(`${k}:`) : `${k}:`
|
|
58
|
-
return `${keyStr} ${valStr}`
|
|
59
|
-
})
|
|
60
|
-
.join(" ")
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function formatRelativeTime(ts: number): string {
|
|
64
|
-
const now = Date.now()
|
|
65
|
-
const diffMs = now - ts
|
|
66
|
-
const diffSec = Math.floor(diffMs / 1000)
|
|
67
|
-
const diffMin = Math.floor(diffSec / 60)
|
|
68
|
-
const diffHour = Math.floor(diffMin / 60)
|
|
69
|
-
|
|
70
|
-
if (diffSec < 60) {
|
|
71
|
-
return "just now"
|
|
72
|
-
}
|
|
73
|
-
if (diffMin < 60) {
|
|
74
|
-
return `${diffMin}m ago`
|
|
75
|
-
}
|
|
76
|
-
if (diffHour < 24) {
|
|
77
|
-
return `${diffHour}h ago`
|
|
78
|
-
}
|
|
79
|
-
return new Date(ts).toLocaleDateString()
|
|
80
|
-
}
|