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/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
+ }