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.
@@ -1,66 +0,0 @@
1
- import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises"
2
- import { join } from "node:path"
3
- import type { NovaAuth, NovaConfig } from "../types.ts"
4
-
5
- const NOVA_DIR = () => join(process.env.HOME ?? "~", ".novacode")
6
- const CONFIG_PATH = () => join(NOVA_DIR(), "config.json")
7
- const AUTH_PATH = () => join(NOVA_DIR(), "auth.json")
8
-
9
- const defaultConfig: NovaConfig = {
10
- provider: "",
11
- model: "",
12
- }
13
-
14
- const defaultAuth: NovaAuth = {
15
- apiKeys: {},
16
- }
17
-
18
- export async function configExists(): Promise<boolean> {
19
- try {
20
- await stat(CONFIG_PATH())
21
- return true
22
- } catch {
23
- return false
24
- }
25
- }
26
-
27
- export async function loadConfig(): Promise<NovaConfig> {
28
- try {
29
- const raw = JSON.parse(await readFile(CONFIG_PATH(), "utf-8"))
30
- return { ...defaultConfig, ...raw }
31
- } catch {
32
- return { ...defaultConfig }
33
- }
34
- }
35
-
36
- export async function loadAuth(): Promise<NovaAuth> {
37
- try {
38
- const raw = JSON.parse(await readFile(AUTH_PATH(), "utf-8"))
39
- return { ...defaultAuth, ...raw }
40
- } catch {
41
- return { ...defaultAuth }
42
- }
43
- }
44
-
45
- async function ensureDir(): Promise<void> {
46
- await mkdir(NOVA_DIR(), { recursive: true })
47
- }
48
-
49
- export async function saveConfig(config: NovaConfig): Promise<void> {
50
- await ensureDir()
51
- await writeFile(CONFIG_PATH(), JSON.stringify(config, null, 2))
52
- }
53
-
54
- export async function saveAuth(auth: NovaAuth): Promise<void> {
55
- await ensureDir()
56
- await writeFile(AUTH_PATH(), JSON.stringify(auth, null, 2))
57
- try {
58
- await chmod(AUTH_PATH(), 0o600)
59
- } catch {
60
- // chmod may fail on some platforms, non-fatal
61
- }
62
- }
63
-
64
- export function getNovaDir(): string {
65
- return NOVA_DIR()
66
- }
package/src/main.ts DELETED
@@ -1,205 +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 type { Session } from "./types.ts"
17
- import { getCurrentVersion, runUpdate } from "./update.ts"
18
-
19
- function parseCli() {
20
- const { values, positionals } = parseArgs({
21
- options: {
22
- help: { type: "boolean", short: "h" },
23
- version: { type: "boolean", short: "v" },
24
- provider: { type: "string" },
25
- model: { type: "string" },
26
- "api-key": { type: "string" },
27
- session: { type: "string", short: "s" },
28
- resume: { type: "boolean" },
29
- n: { type: "string" },
30
- limit: { type: "string" },
31
- all: { type: "boolean" },
32
- },
33
- strict: false,
34
- allowPositionals: true,
35
- })
36
-
37
- return { flags: values, args: positionals }
38
- }
39
-
40
- function findModel(modelId: string, providerId?: string) {
41
- return MODELS.find((m) => {
42
- if (providerId) return m.provider === providerId && m.id === modelId
43
- return m.id === modelId
44
- })
45
- }
46
-
47
- async function main() {
48
- const { flags, args } = parseCli()
49
-
50
- if (flags.version) {
51
- const version = await getCurrentVersion()
52
- console.log(`nova ${version}`)
53
- process.exit(0)
54
- }
55
-
56
- if (flags.help) {
57
- console.log(`nova — open-source coding agent
58
-
59
- Usage:
60
- nova Interactive mode
61
- nova update Update to latest version
62
- nova --session ls List sessions (last 10 by default)
63
- nova --session ls -n N List last N sessions
64
- nova --session rm <id> Delete a specific session
65
- nova --session rm --all Delete all sessions
66
- nova --session <id> Resume a session by ID
67
- nova --resume Resume the most recent session
68
-
69
- Options:
70
- -h, --help Show help
71
- -v, --version Show version
72
- --provider <id> Provider to use
73
- --model <id> Model to use
74
- --api-key <key> API key override
75
- -s, --session <id> Resume/manage session`)
76
- process.exit(0)
77
- }
78
-
79
- // Handle update subcommand
80
- if (args[0] === "update") {
81
- await runUpdate()
82
- return
83
- }
84
-
85
- // Reject positional args — use interactive mode with / commands
86
- if (args.length > 0 && !flags.session) {
87
- console.error(chalk.yellow(`Unknown command: ${args.join(" ")}`))
88
- console.error("Run `nova --help` for usage.")
89
- process.exit(1)
90
- }
91
-
92
- const controller = new AbortController()
93
-
94
- const onSignal = () => {
95
- controller.abort()
96
- process.stderr.write("\nAborted.\n")
97
- process.exit(130)
98
- }
99
- process.on("SIGINT", onSignal)
100
- process.on("SIGTERM", onSignal)
101
-
102
- // First-run onboarding
103
- const config = await ((await configExists()) ? loadConfig() : runOnboarding())
104
- const auth = await loadAuth()
105
-
106
- const store = await getSessionStore()
107
- await store.prune()
108
-
109
- // Handle --session commands (ls, rm)
110
- if (flags.session) {
111
- const sessionFlag = flags.session as string
112
- if (sessionFlag === "ls" || sessionFlag === "list") {
113
- const limit = parseInt((flags.n as string) || (flags.limit as string) || "10", 10)
114
- await handleSessionCommand(store, ["ls"], { limit })
115
- return
116
- }
117
- if (sessionFlag === "rm" || sessionFlag === "delete") {
118
- const id = args[0]
119
- const all = !!flags.all
120
- await handleSessionCommand(store, ["rm", id ?? ""], { all })
121
- return
122
- }
123
- }
124
-
125
- let session: Session | null = null
126
- if (flags.resume) {
127
- session = await store.latest()
128
- if (!session) {
129
- console.error("No recent session found to resume.")
130
- process.exit(1)
131
- }
132
- } else if (flags.session) {
133
- session = await store.get(flags.session as string)
134
- if (!session) {
135
- console.error(`Session not found: ${flags.session}`)
136
- process.exit(1)
137
- }
138
- }
139
-
140
- // CLI overrides or session default or config default
141
- const providerId = (flags.provider as string) || session?.provider || config.provider
142
- const modelId = (flags.model as string) || session?.model || config.model
143
- const apiKey = (flags["api-key"] as string) || auth.apiKeys[providerId]
144
-
145
- const provider = getProvider(providerId)
146
- if (!provider) {
147
- console.error(`Unknown provider: ${providerId}`)
148
- console.error(`Available: ${getProvider("glm") ? "glm, " : ""}gemini, deepseek, openai`)
149
- process.exit(1)
150
- }
151
-
152
- if (!apiKey) {
153
- console.error(
154
- `No API key for ${provider.name}. Set ${provider.envKey} or run nova for onboarding.`,
155
- )
156
- process.exit(1)
157
- }
158
-
159
- const model = findModel(modelId, providerId)
160
- if (!model) {
161
- console.error(`Unknown model: ${modelId}`)
162
- console.error("Available models:")
163
- for (const m of MODELS.filter((m) => m.provider === providerId)) {
164
- console.error(` ${m.id} — ${m.name}`)
165
- }
166
- process.exit(1)
167
- }
168
-
169
- const cwd = process.cwd()
170
- const tools = getAllTools(cwd)
171
- const system = buildSystemPrompt(cwd, tools)
172
-
173
- if (!session) {
174
- session = await store.create(cwd, model.id, providerId)
175
- }
176
-
177
- const sessionId = session.id
178
- const existingMessages = await store.messages(sessionId)
179
-
180
- const agent = new Agent({
181
- api: provider.api,
182
- model,
183
- apiKey,
184
- baseUrl: provider.baseUrl,
185
- system,
186
- tools,
187
- messages: existingMessages,
188
- })
189
-
190
- // Interactive TUI mode
191
- process.off("SIGINT", onSignal)
192
- process.off("SIGTERM", onSignal)
193
- const { interactive } = await import("./tui/app.tsx")
194
- await interactive(agent, store, sessionId)
195
- }
196
-
197
- process.on("unhandledRejection", (reason) => {
198
- console.error("Unhandled rejection:", reason)
199
- process.exit(1)
200
- })
201
-
202
- main().catch((e) => {
203
- console.error("Fatal:", e)
204
- process.exit(1)
205
- })
@@ -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,269 +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
- let usage: Usage = { in: 0, out: 0 }
101
- const content: ContentPart[] = []
102
-
103
- try {
104
- const baseUrl = opts.baseUrl || "https://generativelanguage.googleapis.com"
105
- const url = `${baseUrl}/v1beta/models/${opts.model.id}:streamGenerateContent?alt=sse&key=${opts.apiKey}`
106
-
107
- const body = {
108
- contents: msgsToGemini(opts.messages),
109
- system_instruction: opts.system ? { parts: [{ text: opts.system }] } : undefined,
110
- tools: opts.tools.length > 0 ? toolsToGemini(opts.tools) : undefined,
111
- generationConfig: {
112
- thinkingConfig: opts.model.supportsThinking ? { thinkingLevel: "low" } : undefined,
113
- },
114
- }
115
-
116
- const response = await fetch(url, {
117
- method: "POST",
118
- headers: {
119
- "Content-Type": "application/json",
120
- "Api-Revision": "2026-05-20",
121
- },
122
- body: JSON.stringify(body),
123
- signal: opts.signal,
124
- })
125
-
126
- if (!response.ok) {
127
- const text = await response.text()
128
- let msg = text
129
- try {
130
- const json = JSON.parse(text)
131
- msg = json.error?.message || json.message || text
132
- } catch {
133
- /* use raw text */
134
- }
135
-
136
- const errorMsg = `Gemini Error (${response.status}): ${msg}`
137
- es.push({ type: "text_delta", text: errorMsg })
138
- es.finish({
139
- content: [{ type: "text", text: errorMsg }],
140
- usage: { in: 0, out: 0 },
141
- stop: "error",
142
- })
143
- return
144
- }
145
-
146
- const reader = response.body?.getReader()
147
- if (!reader) {
148
- es.finish({ content: [], usage: { in: 0, out: 0 }, stop: "error" })
149
- return
150
- }
151
-
152
- const decoder = new TextDecoder()
153
- let buffer = ""
154
- let stop: StopReason = "stop"
155
-
156
- while (true) {
157
- const { done, value } = await reader.read()
158
- if (done) break
159
-
160
- buffer += decoder.decode(value, { stream: true })
161
- const lines = buffer.split("\n")
162
- buffer = lines.pop() ?? ""
163
-
164
- for (const line of lines) {
165
- const trimmed = line.trim()
166
- if (!trimmed?.startsWith("data: ")) continue
167
- const data = trimmed.slice(6)
168
-
169
- try {
170
- const chunk = JSON.parse(data)
171
- const candidate = chunk.candidates?.[0]
172
-
173
- // Handle usage metadata
174
- if (chunk.usageMetadata) {
175
- usage = {
176
- in: chunk.usageMetadata.promptTokenCount || usage.in,
177
- out: chunk.usageMetadata.candidatesTokenCount || usage.out,
178
- }
179
- es.push({ type: "usage", usage })
180
- }
181
-
182
- if (!candidate) continue
183
-
184
- // Map finish reason
185
- if (candidate.finishReason) {
186
- const reason = candidate.finishReason
187
- if (reason === "STOP") stop = "stop"
188
- else if (reason === "MAX_TOKENS") stop = "length"
189
- else if (reason === "SAFETY" || reason === "RECITATION" || reason === "OTHER")
190
- stop = "error"
191
- }
192
-
193
- const parts = candidate.content?.parts
194
- if (parts) {
195
- for (const part of parts) {
196
- const sig = part.thought_signature || part.thoughtSignature
197
-
198
- // Handle text and thinking deltas
199
- if (part.text) {
200
- if (part.thought === true || typeof part.thought === "string") {
201
- const thoughtText = typeof part.thought === "string" ? part.thought : part.text
202
- es.push({ type: "thinking_delta", text: thoughtText })
203
- const last = content[content.length - 1]
204
- if (last?.type === "thinking") {
205
- last.text += thoughtText
206
- } else {
207
- content.push({ type: "thinking", text: thoughtText, signature: sig })
208
- }
209
- } else {
210
- es.push({ type: "text_delta", text: part.text })
211
- const last = content[content.length - 1]
212
- if (last?.type === "text") {
213
- last.text += part.text
214
- } else {
215
- content.push({ type: "text", text: part.text, signature: sig })
216
- }
217
- }
218
- }
219
-
220
- // Handle function calls (can be snake_case or camelCase in some API versions)
221
- const fc = part.functionCall || part.function_call
222
- if (fc) {
223
- const name = fc.name
224
- const args = (fc.args as Record<string, unknown>) || {}
225
- const id = `call_${Math.random().toString(36).slice(2, 9)}`
226
-
227
- const toolCall: ContentPart = {
228
- type: "tool_call",
229
- id,
230
- name,
231
- args,
232
- signature: sig,
233
- }
234
- content.push(toolCall)
235
- es.push({ type: "tool_call", call: toolCall })
236
- stop = "tool_use"
237
- }
238
- }
239
- }
240
- } catch (_e) {
241
- if (data.trim() !== "" && data.trim() !== "[DONE]") {
242
- // skip noise
243
- }
244
- }
245
- }
246
- }
247
-
248
- es.finish({ content, usage, stop })
249
- } catch (e) {
250
- if (opts.signal?.aborted) {
251
- es.finish({
252
- content,
253
- usage,
254
- stop: "aborted",
255
- })
256
- return
257
- }
258
- const errorMsg = `Gemini Network/Request Error: ${e instanceof Error ? e.message : String(e)}`
259
- es.push({ type: "text_delta", text: errorMsg })
260
- es.finish({
261
- content: [{ type: "text", text: errorMsg }],
262
- usage: { in: 0, out: 0 },
263
- stop: "error",
264
- })
265
- }
266
- })()
267
-
268
- return es
269
- }