novacode 0.2.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/LICENSE +201 -0
- package/README.md +89 -0
- package/package.json +56 -0
- package/src/agent/agent.ts +87 -0
- package/src/agent/loop.ts +218 -0
- package/src/agent/prompt.ts +50 -0
- package/src/commands/compact.ts +28 -0
- package/src/commands/index.ts +62 -0
- package/src/commands/models.ts +86 -0
- package/src/commands/providers.ts +222 -0
- package/src/commands/session.ts +40 -0
- package/src/config/providers.ts +199 -0
- package/src/config/store.ts +67 -0
- package/src/main.ts +169 -0
- package/src/onboarding/wizard.ts +58 -0
- package/src/provider/gemini.ts +254 -0
- package/src/provider/openai.ts +218 -0
- package/src/provider/registry.ts +62 -0
- package/src/provider/stream.ts +77 -0
- package/src/session/compact.ts +126 -0
- package/src/session/store.ts +206 -0
- package/src/tools/fs.ts +195 -0
- package/src/tools/git.ts +82 -0
- package/src/tools/index.ts +33 -0
- package/src/tools/search.ts +252 -0
- package/src/tools/shell.ts +89 -0
- package/src/tools/web.ts +239 -0
- package/src/tui/app.tsx +517 -0
- package/src/tui/markdown.ts +62 -0
- package/src/tui/print.ts +75 -0
- package/src/types.ts +233 -0
- package/src/util.ts +88 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
/** Commands */
|
|
228
|
+
|
|
229
|
+
export interface Cmd {
|
|
230
|
+
name: string
|
|
231
|
+
desc: string
|
|
232
|
+
aliases?: string[]
|
|
233
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path"
|
|
2
|
+
import chalk from "chalk"
|
|
3
|
+
import type { ContentPart, 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 consolidate(parts: ContentPart[]): ContentPart[] {
|
|
25
|
+
if (parts.length === 0) return parts
|
|
26
|
+
const out: ContentPart[] = []
|
|
27
|
+
for (const p of parts) {
|
|
28
|
+
const last = out[out.length - 1]
|
|
29
|
+
if (last?.type === "text" && p.type === "text") {
|
|
30
|
+
last.text += p.text
|
|
31
|
+
} else if (last?.type === "thinking" && p.type === "thinking") {
|
|
32
|
+
last.text += p.text
|
|
33
|
+
} else {
|
|
34
|
+
out.push({ ...p })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const hasTool = out.some((p) => p.type === "tool_call")
|
|
39
|
+
if (hasTool) {
|
|
40
|
+
return out.filter((p) => {
|
|
41
|
+
if (p.type === "text") {
|
|
42
|
+
return p.text.trim().length > 0
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getRelativeIfInside(cwd: string, filePath: string): string {
|
|
52
|
+
if (filePath === cwd || filePath.startsWith(`${cwd}/`)) {
|
|
53
|
+
return relative(cwd, filePath) || "."
|
|
54
|
+
}
|
|
55
|
+
return filePath
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function makeRelative(val: string): string {
|
|
59
|
+
if (typeof val !== "string") return val
|
|
60
|
+
|
|
61
|
+
let pathVal = val
|
|
62
|
+
let prefix = ""
|
|
63
|
+
if (val.startsWith("file://")) {
|
|
64
|
+
pathVal = val.slice(7)
|
|
65
|
+
prefix = "file://"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isAbsolute(pathVal)) {
|
|
69
|
+
const cwd = process.cwd()
|
|
70
|
+
return prefix + getRelativeIfInside(cwd, pathVal)
|
|
71
|
+
}
|
|
72
|
+
return val
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatToolArgs(
|
|
76
|
+
args: Record<string, unknown> | undefined,
|
|
77
|
+
useChalk = false,
|
|
78
|
+
): string {
|
|
79
|
+
if (!args) return ""
|
|
80
|
+
return Object.entries(args)
|
|
81
|
+
.map(([k, v]) => {
|
|
82
|
+
const val = typeof v === "string" ? makeRelative(v) : JSON.stringify(v)
|
|
83
|
+
const valStr = val.length > 40 ? `${val.slice(0, 40)}…` : val
|
|
84
|
+
const keyStr = useChalk ? chalk.dim(`${k}:`) : `${k}:`
|
|
85
|
+
return `${keyStr} ${valStr}`
|
|
86
|
+
})
|
|
87
|
+
.join(" ")
|
|
88
|
+
}
|