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.
package/src/main.ts DELETED
@@ -1,175 +0,0 @@
1
- #!/usr/bin/env node
2
- import { parseArgs } from "node:util"
3
- /**
4
- * Entry point for the nova CLI.
5
- * Handles configuration, CLI flags, and runs interactive TUI mode.
6
- */
7
- import chalk from "chalk"
8
- import { Agent } from "./agent/agent.ts"
9
- import { buildSystemPrompt } from "./agent/prompt.ts"
10
- import { handleSessionCommand } from "./commands/session.ts"
11
- import { getProvider, MODELS } from "./config/providers.ts"
12
- import { configExists, loadAuth, loadConfig } from "./config/store.ts"
13
- import { runOnboarding } from "./onboarding/wizard.ts"
14
- import { getSessionStore } from "./session/store.ts"
15
- import { getAllTools } from "./tools/index.ts"
16
- import { getCurrentVersion, runUpdate } from "./update.ts"
17
-
18
- function parseCli() {
19
- const { values, positionals } = parseArgs({
20
- options: {
21
- help: { type: "boolean", short: "h" },
22
- version: { type: "boolean", short: "v" },
23
- provider: { type: "string" },
24
- model: { type: "string" },
25
- "api-key": { type: "string" },
26
- session: { type: "string", short: "s" },
27
- },
28
- strict: true,
29
- allowPositionals: true,
30
- })
31
-
32
- return { flags: values, args: positionals }
33
- }
34
-
35
- function findModel(modelId: string, providerId?: string) {
36
- return MODELS.find((m) => {
37
- if (providerId) return m.provider === providerId && m.id === modelId
38
- return m.id === modelId
39
- })
40
- }
41
-
42
- async function main() {
43
- const { flags, args } = parseCli()
44
-
45
- if (flags.version) {
46
- const version = await getCurrentVersion()
47
- console.log(`nova ${version}`)
48
- process.exit(0)
49
- }
50
-
51
- if (flags.help) {
52
- console.log(`nova — open-source coding agent
53
-
54
- Usage:
55
- nova Interactive mode
56
- nova update Update to latest version
57
- nova session <cmd> Session management (list, delete)
58
- nova --session <id> Resume a session
59
-
60
- Options:
61
- -h, --help Show help
62
- -v, --version Show version
63
- --provider <id> Provider to use
64
- --model <id> Model to use
65
- --api-key <key> API key override
66
- -s, --session <id> Resume session by ID`)
67
- process.exit(0)
68
- }
69
-
70
- // Handle session subcommand
71
- if (args[0] === "session") {
72
- await handleSessionCommand(args.slice(1))
73
- return
74
- }
75
-
76
- // Handle update subcommand
77
- if (args[0] === "update") {
78
- await runUpdate()
79
- return
80
- }
81
-
82
- // Reject positional args — use interactive mode with / commands
83
- if (args.length > 0) {
84
- console.error(chalk.yellow(`Unknown command: ${args.join(" ")}`))
85
- console.error("Run `nova --help` for usage.")
86
- process.exit(1)
87
- }
88
-
89
- const controller = new AbortController()
90
-
91
- const onSignal = () => {
92
- controller.abort()
93
- process.stderr.write("\nAborted.\n")
94
- process.exit(130)
95
- }
96
- process.on("SIGINT", onSignal)
97
- process.on("SIGTERM", onSignal)
98
-
99
- // First-run onboarding
100
- const config = await ((await configExists()) ? loadConfig() : runOnboarding())
101
- const auth = await loadAuth()
102
-
103
- // CLI overrides
104
- const providerId = (flags.provider as string) || config.provider
105
- const modelId = (flags.model as string) || config.model
106
- const apiKey = (flags["api-key"] as string) || auth.apiKeys[providerId]
107
-
108
- const provider = getProvider(providerId)
109
- if (!provider) {
110
- console.error(`Unknown provider: ${providerId}`)
111
- console.error(`Available: ${getProvider("glm") ? "glm, " : ""}gemini, deepseek, openai`)
112
- process.exit(1)
113
- }
114
-
115
- if (!apiKey) {
116
- console.error(
117
- `No API key for ${provider.name}. Set ${provider.envKey} or run nova for onboarding.`,
118
- )
119
- process.exit(1)
120
- }
121
-
122
- const model = findModel(modelId, providerId)
123
- if (!model) {
124
- console.error(`Unknown model: ${modelId}`)
125
- console.error("Available models:")
126
- for (const m of MODELS.filter((m) => m.provider === providerId)) {
127
- console.error(` ${m.id} — ${m.name}`)
128
- }
129
- process.exit(1)
130
- }
131
-
132
- const cwd = process.cwd()
133
- const tools = getAllTools(cwd)
134
- const system = buildSystemPrompt(cwd, tools)
135
-
136
- // Session persistence
137
- const store = getSessionStore()
138
- const session = flags.session
139
- ? store.get(flags.session as string)
140
- : store.create(cwd, model.id, providerId)
141
-
142
- if (flags.session && !session) {
143
- console.error(`Session not found: ${flags.session}`)
144
- process.exit(1)
145
- }
146
-
147
- const sessionId = session!.id
148
- const existingMessages = store.messages(sessionId)
149
-
150
- const agent = new Agent({
151
- api: provider.api,
152
- model,
153
- apiKey,
154
- baseUrl: provider.baseUrl,
155
- system,
156
- tools,
157
- messages: existingMessages,
158
- })
159
-
160
- // Interactive TUI mode
161
- process.off("SIGINT", onSignal)
162
- process.off("SIGTERM", onSignal)
163
- const { interactive } = await import("./tui/app.tsx")
164
- await interactive(agent, store, sessionId)
165
- }
166
-
167
- process.on("unhandledRejection", (reason) => {
168
- console.error("Unhandled rejection:", reason)
169
- process.exit(1)
170
- })
171
-
172
- main().catch((e) => {
173
- console.error("Fatal:", e)
174
- process.exit(1)
175
- })
@@ -1,54 +0,0 @@
1
- import chalk from "chalk"
2
- import { getModelsForProvider, getProvider, PROVIDERS } from "../config/providers.ts"
3
- import { saveAuth, saveConfig } from "../config/store.ts"
4
- import { standalonePassword, standaloneSelect } from "../tui/prompts.tsx"
5
- import type { NovaConfig } from "../types.ts"
6
-
7
- export async function runOnboarding(): Promise<NovaConfig> {
8
- console.log(chalk.bold.cyan("\n⚡ Nova — your coding companion\n"))
9
-
10
- const providerId = await standaloneSelect(
11
- "Pick a provider",
12
- PROVIDERS.map((p) => ({ value: p.id, label: p.name })),
13
- )
14
- if (!providerId) {
15
- console.log(chalk.dim("Cancelled"))
16
- process.exit(0)
17
- }
18
-
19
- const provider = getProvider(providerId)
20
- if (!provider) {
21
- console.log(chalk.red("Unknown provider"))
22
- process.exit(1)
23
- }
24
-
25
- const apiKey = await standalonePassword(`Enter ${provider.name} API key`)
26
- if (!apiKey) {
27
- console.log(chalk.dim("Cancelled"))
28
- process.exit(0)
29
- }
30
-
31
- const models = getModelsForProvider(providerId)
32
- const modelId = await standaloneSelect(
33
- "Pick a default model",
34
- models.map((m) => ({
35
- value: m.id,
36
- label: `${m.name} (${(m.contextWindow / 1000).toFixed(0)}k ctx)`,
37
- })),
38
- )
39
- if (!modelId) {
40
- console.log(chalk.dim("Cancelled"))
41
- process.exit(0)
42
- }
43
-
44
- const config: NovaConfig = {
45
- provider: providerId,
46
- model: modelId,
47
- }
48
-
49
- await saveConfig(config)
50
- await saveAuth({ apiKeys: { [providerId]: apiKey } })
51
-
52
- console.log(chalk.green("\n✓ Ready. Type your prompt or /help for commands\n"))
53
- return config
54
- }
@@ -1,261 +0,0 @@
1
- import type {
2
- AssistantResult,
3
- ContentPart,
4
- Msg,
5
- StopReason,
6
- StreamEvent,
7
- StreamFn,
8
- StreamOpts,
9
- ToolDef,
10
- Usage,
11
- } from "../types.ts"
12
- import { EventStream } from "./stream.ts"
13
-
14
- interface GeminiPart {
15
- text?: string
16
- thought?: boolean | string
17
- inline_data?: { mime_type: string; data: string }
18
- function_call?: { name: string; args: Record<string, unknown> }
19
- function_response?: { name: string; response: Record<string, unknown> }
20
- thought_signature?: string
21
- }
22
-
23
- interface GeminiContent {
24
- role: "user" | "model"
25
- parts: GeminiPart[]
26
- }
27
-
28
- /**
29
- * Maps our internal Msg format to the Gemini 'Content' format.
30
- * Groups consecutive tool_result messages into a single Gemini message.
31
- */
32
- function msgsToGemini(messages: Msg[]): GeminiContent[] {
33
- const contents: GeminiContent[] = []
34
-
35
- for (const msg of messages) {
36
- if (msg.role === "user") {
37
- const parts: GeminiPart[] =
38
- typeof msg.content === "string"
39
- ? [{ text: msg.content }]
40
- : msg.content.map((c) => {
41
- if (c.type === "text") return { text: c.text }
42
- if (c.type === "image") return { inline_data: { mime_type: c.mime, data: c.data } }
43
- return { text: "" }
44
- })
45
- contents.push({ role: "user", parts })
46
- } else if (msg.role === "assistant") {
47
- const parts: GeminiPart[] = msg.content.map((c) => {
48
- if (c.type === "text") return { text: c.text, thought_signature: c.signature }
49
- if (c.type === "thinking")
50
- return { thought: true, text: c.text, thought_signature: c.signature }
51
- if (c.type === "tool_call")
52
- return { function_call: { name: c.name, args: c.args }, thought_signature: c.signature }
53
- return { text: "" }
54
- })
55
- contents.push({ role: "model", parts })
56
- } else if (msg.role === "tool_result") {
57
- const part: GeminiPart = {
58
- function_response: {
59
- name: msg.tool,
60
- response: {
61
- content: msg.content
62
- .map((c) => (c.type === "text" ? c.text : JSON.stringify(c)))
63
- .join("\n"),
64
- },
65
- },
66
- }
67
-
68
- const last = contents[contents.length - 1]
69
- // Gemini requires alternating roles; multiple function_responses group into one 'user' message.
70
- if (last && last.role === "user" && last.parts.some((p) => p.function_response)) {
71
- last.parts.push(part)
72
- } else {
73
- contents.push({ role: "user", parts: [part] })
74
- }
75
- }
76
- }
77
-
78
- return contents
79
- }
80
-
81
- function toolsToGemini(tools: ToolDef[]): unknown[] {
82
- if (tools.length === 0) return []
83
- return [
84
- {
85
- function_declarations: tools.map((t) => ({
86
- name: t.name,
87
- description: t.description,
88
- parameters: t.parameters,
89
- })),
90
- },
91
- ]
92
- }
93
-
94
- export const streamGemini: StreamFn = (
95
- opts: StreamOpts,
96
- ): EventStream<StreamEvent, AssistantResult> => {
97
- const es = new EventStream<StreamEvent, AssistantResult>()
98
-
99
- ;(async () => {
100
- try {
101
- const baseUrl = opts.baseUrl || "https://generativelanguage.googleapis.com"
102
- const url = `${baseUrl}/v1beta/models/${opts.model.id}:streamGenerateContent?alt=sse&key=${opts.apiKey}`
103
-
104
- const body = {
105
- contents: msgsToGemini(opts.messages),
106
- system_instruction: opts.system ? { parts: [{ text: opts.system }] } : undefined,
107
- tools: opts.tools.length > 0 ? toolsToGemini(opts.tools) : undefined,
108
- generationConfig: {
109
- thinkingConfig: opts.model.supportsThinking ? { thinkingLevel: "low" } : undefined,
110
- },
111
- }
112
-
113
- const response = await fetch(url, {
114
- method: "POST",
115
- headers: {
116
- "Content-Type": "application/json",
117
- "Api-Revision": "2026-05-20",
118
- },
119
- body: JSON.stringify(body),
120
- signal: opts.signal,
121
- })
122
-
123
- if (!response.ok) {
124
- const text = await response.text()
125
- let msg = text
126
- try {
127
- const json = JSON.parse(text)
128
- msg = json.error?.message || json.message || text
129
- } catch {
130
- /* use raw text */
131
- }
132
-
133
- const errorMsg = `Gemini Error (${response.status}): ${msg}`
134
- es.push({ type: "text_delta", text: errorMsg })
135
- es.finish({
136
- content: [{ type: "text", text: errorMsg }],
137
- usage: { in: 0, out: 0 },
138
- stop: "error",
139
- })
140
- return
141
- }
142
-
143
- const reader = response.body?.getReader()
144
- if (!reader) {
145
- es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
146
- return
147
- }
148
-
149
- const decoder = new TextDecoder()
150
- let buffer = ""
151
- let usage: Usage = { in: 0, out: 0 }
152
- let stop: StopReason = "stop"
153
- const content: ContentPart[] = []
154
-
155
- while (true) {
156
- const { done, value } = await reader.read()
157
- if (done) break
158
-
159
- buffer += decoder.decode(value, { stream: true })
160
- const lines = buffer.split("\n")
161
- buffer = lines.pop() ?? ""
162
-
163
- for (const line of lines) {
164
- const trimmed = line.trim()
165
- if (!trimmed?.startsWith("data: ")) continue
166
- const data = trimmed.slice(6)
167
-
168
- try {
169
- const chunk = JSON.parse(data)
170
- const candidate = chunk.candidates?.[0]
171
-
172
- // Handle usage metadata
173
- if (chunk.usageMetadata) {
174
- usage = {
175
- in: chunk.usageMetadata.promptTokenCount || usage.in,
176
- out: chunk.usageMetadata.candidatesTokenCount || usage.out,
177
- }
178
- es.push({ type: "usage", usage })
179
- }
180
-
181
- if (!candidate) continue
182
-
183
- // Map finish reason
184
- if (candidate.finishReason) {
185
- const reason = candidate.finishReason
186
- if (reason === "STOP") stop = "stop"
187
- else if (reason === "MAX_TOKENS") stop = "length"
188
- else if (reason === "SAFETY" || reason === "RECITATION" || reason === "OTHER")
189
- stop = "error"
190
- }
191
-
192
- const parts = candidate.content?.parts
193
- if (parts) {
194
- for (const part of parts) {
195
- const sig = part.thought_signature || part.thoughtSignature
196
-
197
- // Handle text and thinking deltas
198
- if (part.text) {
199
- if (part.thought === true || typeof part.thought === "string") {
200
- const thoughtText = typeof part.thought === "string" ? part.thought : part.text
201
- es.push({ type: "thinking_delta", text: thoughtText })
202
- const last = content[content.length - 1]
203
- if (last?.type === "thinking") {
204
- last.text += thoughtText
205
- } else {
206
- content.push({ type: "thinking", text: thoughtText, signature: sig })
207
- }
208
- } else {
209
- es.push({ type: "text_delta", text: part.text })
210
- const last = content[content.length - 1]
211
- if (last?.type === "text") {
212
- last.text += part.text
213
- } else {
214
- content.push({ type: "text", text: part.text, signature: sig })
215
- }
216
- }
217
- }
218
-
219
- // Handle function calls (can be snake_case or camelCase in some API versions)
220
- const fc = part.functionCall || part.function_call
221
- if (fc) {
222
- const name = fc.name
223
- const args = (fc.args as Record<string, unknown>) || {}
224
- const id = `call_${Math.random().toString(36).slice(2, 9)}`
225
-
226
- const toolCall: ContentPart = {
227
- type: "tool_call",
228
- id,
229
- name,
230
- args,
231
- signature: sig,
232
- }
233
- content.push(toolCall)
234
- es.push({ type: "tool_call", call: toolCall })
235
- stop = "tool_use"
236
- }
237
- }
238
- }
239
- } catch (_e) {
240
- if (data.trim() !== "" && data.trim() !== "[DONE]") {
241
- // skip noise
242
- }
243
- }
244
- }
245
- }
246
-
247
- es.finish({ content, usage, stop })
248
- } catch (e) {
249
- if (opts.signal?.aborted) return
250
- const errorMsg = `Gemini Network/Request Error: ${e instanceof Error ? e.message : String(e)}`
251
- es.push({ type: "text_delta", text: errorMsg })
252
- es.finish({
253
- content: [{ type: "text", text: errorMsg }],
254
- usage: { in: 0, out: 0 },
255
- stop: "error",
256
- })
257
- }
258
- })()
259
-
260
- return es
261
- }
@@ -1,215 +0,0 @@
1
- import type {
2
- AssistantResult,
3
- Msg,
4
- StopReason,
5
- StreamEvent,
6
- StreamFn,
7
- StreamOpts,
8
- ToolDef,
9
- Usage,
10
- } from "../types.ts"
11
- import { EventStream } from "./stream.ts"
12
-
13
- function msgToOpenAI(msg: Msg): Record<string, unknown> {
14
- if (msg.role === "user") {
15
- return {
16
- role: "user",
17
- content:
18
- typeof msg.content === "string"
19
- ? msg.content
20
- : msg.content.map((c) => {
21
- if (c.type === "text") return { type: "text", text: c.text }
22
- if (c.type === "image")
23
- return { type: "image_url", image_url: { url: `data:${c.mime};base64,${c.data}` } }
24
- return { type: "text", text: "" }
25
- }),
26
- }
27
- }
28
- if (msg.role === "assistant") {
29
- const textParts: string[] = []
30
- const toolCalls: unknown[] = []
31
-
32
- for (const c of msg.content) {
33
- if (c.type === "text") textParts.push(c.text)
34
- // thinking parts are internal — never sent back to the API
35
- if (c.type === "tool_call")
36
- toolCalls.push({
37
- type: "function",
38
- id: c.id,
39
- function: { name: c.name, arguments: JSON.stringify(c.args) },
40
- })
41
- }
42
-
43
- const result: Record<string, unknown> = {
44
- role: "assistant",
45
- content: textParts.length > 0 ? textParts.join("") : null,
46
- }
47
- if (toolCalls.length > 0) result.tool_calls = toolCalls
48
- return result
49
- }
50
- // tool_result
51
- if (msg.role === "tool_result") {
52
- return {
53
- role: "tool",
54
- tool_call_id: msg.callId,
55
- content: msg.content.map((c) => (c.type === "text" ? c.text : JSON.stringify(c))).join("\n"),
56
- }
57
- }
58
- return { role: "user", content: "" }
59
- }
60
-
61
- function toolsToOpenAI(tools: ToolDef[]): unknown[] {
62
- return tools.map((t) => ({
63
- type: "function",
64
- function: {
65
- name: t.name,
66
- description: t.description,
67
- parameters: t.parameters,
68
- },
69
- }))
70
- }
71
-
72
- export const streamOpenAI: StreamFn = (
73
- opts: StreamOpts,
74
- ): EventStream<StreamEvent, AssistantResult> => {
75
- const es = new EventStream<StreamEvent, AssistantResult>()
76
-
77
- ;(async () => {
78
- try {
79
- const body = {
80
- model: opts.model.id,
81
- messages: [{ role: "system", content: opts.system }, ...opts.messages.map(msgToOpenAI)],
82
- tools: opts.tools.length > 0 ? toolsToOpenAI(opts.tools) : undefined,
83
- stream: true,
84
- }
85
-
86
- const response = await fetch(`${opts.baseUrl}/chat/completions`, {
87
- method: "POST",
88
- headers: {
89
- "Content-Type": "application/json",
90
- Authorization: `Bearer ${opts.apiKey}`,
91
- },
92
- body: JSON.stringify(body),
93
- signal: opts.signal,
94
- })
95
-
96
- if (!response.ok) {
97
- const text = await response.text()
98
- const errorMsg = `API error ${response.status}: ${text}`
99
- es.push({ type: "text_delta", text: errorMsg })
100
- es.finish({
101
- content: [{ type: "text", text: errorMsg }],
102
- usage: { in: 0, out: 0 },
103
- stop: "error",
104
- })
105
- return
106
- }
107
-
108
- const reader = response.body?.getReader()
109
- if (!reader) {
110
- es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
111
- return
112
- }
113
-
114
- const decoder = new TextDecoder()
115
- let buffer = ""
116
- const currentToolCalls = new Map<number, { id: string; name: string; args: string }>()
117
- let usage: Usage = { in: 0, out: 0 }
118
- let textContent = ""
119
- let stop = "stop"
120
-
121
- while (true) {
122
- const { done, value } = await reader.read()
123
- if (done) break
124
-
125
- buffer += decoder.decode(value, { stream: true })
126
- const lines = buffer.split("\n")
127
- buffer = lines.pop() ?? ""
128
-
129
- for (const line of lines) {
130
- const trimmed = line.trim()
131
- if (!trimmed?.startsWith("data: ")) continue
132
- const data = trimmed.slice(6)
133
- if (data === "[DONE]") continue
134
-
135
- try {
136
- const chunk = JSON.parse(data)
137
- const delta = chunk.choices?.[0]?.delta
138
- if (!delta) continue
139
-
140
- if (delta.content) {
141
- es.push({ type: "text_delta", text: delta.content })
142
- textContent += delta.content
143
- }
144
-
145
- if (delta.tool_calls) {
146
- for (const tc of delta.tool_calls) {
147
- const idx = tc.index ?? 0
148
- if (!currentToolCalls.has(idx)) {
149
- currentToolCalls.set(idx, {
150
- id: tc.id ?? "",
151
- name: tc.function?.name ?? "",
152
- args: "",
153
- })
154
- }
155
- const existing = currentToolCalls.get(idx)!
156
- if (tc.id) existing.id = tc.id
157
- if (tc.function?.name) existing.name = tc.function.name
158
- if (tc.function?.arguments) existing.args += tc.function.arguments
159
- }
160
- }
161
-
162
- if (chunk.usage) {
163
- usage = {
164
- in: chunk.usage.prompt_tokens ?? 0,
165
- out: chunk.usage.completion_tokens ?? 0,
166
- }
167
- es.push({ type: "usage", usage })
168
- }
169
-
170
- const finishReason = chunk.choices?.[0]?.finish_reason
171
- if (finishReason) stop = finishReason
172
- } catch {
173
- // Skip malformed JSON chunks
174
- }
175
- }
176
- }
177
-
178
- const content: AssistantResult["content"] = []
179
- if (textContent) {
180
- content.push({ type: "text", text: textContent })
181
- }
182
- for (const [, tc] of currentToolCalls) {
183
- content.push({
184
- type: "tool_call",
185
- id: tc.id,
186
- name: tc.name,
187
- args: JSON.parse(tc.args || "{}"),
188
- })
189
- es.push({
190
- type: "tool_call",
191
- call: {
192
- type: "tool_call",
193
- id: tc.id,
194
- name: tc.name,
195
- args: JSON.parse(tc.args || "{}"),
196
- },
197
- })
198
- stop = "tool_use"
199
- }
200
-
201
- es.finish({ content, usage, stop: stop as StopReason })
202
- } catch (e) {
203
- if (opts.signal?.aborted) return
204
- const errorMsg = `Unexpected error: ${e instanceof Error ? e.message : String(e)}`
205
- es.push({ type: "text_delta", text: errorMsg })
206
- es.finish({
207
- content: [{ type: "text", text: errorMsg }],
208
- usage: { in: 0, out: 0 },
209
- stop: "error",
210
- })
211
- }
212
- })()
213
-
214
- return es
215
- }