novacode 0.3.0 → 0.5.1

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/README.md CHANGED
@@ -1,11 +1,12 @@
1
- # novacode
1
+ # NovaCode
2
2
 
3
- Open-source, multi-provider coding agent. Built with Bun.
3
+ Open-source, multi-provider coding agent.
4
4
 
5
5
  > **Currently in early development.**
6
6
 
7
- ## Install
7
+ <img width="1164" height="720" alt="result" src="https://github.com/user-attachments/assets/a456c41a-ec19-4a4d-b3b7-180e6b83acc3" />
8
8
 
9
+ ## Install
9
10
  Requires [Bun](https://bun.sh) >= 1.3.
10
11
 
11
12
  ```bash
@@ -31,10 +32,9 @@ nova
31
32
  ### 2. First-run setup
32
33
 
33
34
  On first launch, nova walks you through a quick setup:
34
-
35
- 1. **Pick a provider** — GLM (Z.AI), Gemini (Google), DeepSeek, or OpenAI
36
- 2. **Enter your API key** — stored securely in `~/.novacode/auth.json`
37
- 3. **Pick a default model** — choose from the provider's available models
35
+ 1. **Pick a provider**
36
+ 2. **Enter your API key**
37
+ 3. **Pick a default model**
38
38
 
39
39
  That's it. You're ready to go.
40
40
 
@@ -55,33 +55,16 @@ nova "explain the auth module in this project"
55
55
  nova "fix the type error in src/utils.ts"
56
56
  ```
57
57
 
58
- ### 4. CLI flags
58
+ ### 4. Flags & commands
59
59
 
60
- ```bash
61
- nova # interactive mode
62
- nova "your prompt" # print mode
63
- nova --provider gemini # override provider
64
- nova --model gemini-2.5-pro # override model
65
- nova --api-key <key> # override API key
66
- nova -s <session-id> # resume a previous session
67
- nova session list # list saved sessions
68
- nova session delete <id> # delete a session
69
- nova -v # show version
70
- nova -h # show help
71
- ```
60
+ Available flags: `--provider`, `--model`, `--api-key`, `-s` (resume session)
61
+
62
+ Session commands: `nova session list`, `nova session delete <id>`
63
+
64
+ Run `nova -h` or type `/help` in interactive mode to see everything.
72
65
 
73
66
  ### Supported Providers
74
67
 
75
68
  GLM (Z.AI), Gemini (Google), DeepSeek, OpenAI
76
69
 
77
- You can set API keys via environment variables or let the onboarding wizard store them in `~/.novacode/auth.json`.
78
-
79
- ## Build from Source
80
70
 
81
- ```bash
82
- git clone https://github.com/rwitesh/novacode.git
83
- cd novacode
84
- bun install
85
- bun run dev # run with watch mode
86
- bun run build # compile to binary
87
- ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novacode",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Open-source multi-provider coding agent. Bun-native.",
5
5
  "type": "module",
6
6
  "module": "src/main.ts",
@@ -2,6 +2,7 @@ import chalk from "chalk"
2
2
  import type { Agent } from "../agent/agent.ts"
3
3
  import type { SessionStore } from "../session/store.ts"
4
4
  import type { Cmd } from "../types.ts"
5
+ import { checkForUpdate, runUpdate } from "../update.ts"
5
6
  import { handleCompact } from "./compact.ts"
6
7
  import { handleModels } from "./models.ts"
7
8
  import { handleProviders } from "./providers.ts"
@@ -10,6 +11,7 @@ export const COMMANDS: Cmd[] = [
10
11
  { name: "models", desc: "Switch model", aliases: ["model"] },
11
12
  { name: "providers", desc: "Manage providers", aliases: ["prov", "config", "cfg"] },
12
13
  { name: "compact", desc: "Compact context" },
14
+ { name: "update", desc: "Update novacode" },
13
15
  { name: "help", desc: "Show help" },
14
16
  { name: "clear", desc: "Clear screen" },
15
17
  { name: "quit", desc: "Exit (Ctrl+D)", aliases: ["exit"] },
@@ -19,6 +21,10 @@ const HELP = `
19
21
  ${chalk.bold("Commands:")}
20
22
  ${COMMANDS.map((c) => ` /${c.name.padEnd(12)} ${c.desc}`).join("\n")}
21
23
 
24
+ ${chalk.bold("CLI:")}
25
+ nova update Update to latest version
26
+ nova session ls List sessions
27
+
22
28
  ${chalk.dim("Keys:")}
23
29
  Esc Abort
24
30
  ↑ / ↓ History
@@ -45,6 +51,8 @@ export async function dispatch(
45
51
  case "compact":
46
52
  if (!store || !sessionId) return chalk.red("Session store not available")
47
53
  return handleCompact(agent, store, sessionId)
54
+ case "update":
55
+ return handleUpdate()
48
56
  case "help":
49
57
  return HELP
50
58
  case "clear":
@@ -60,3 +68,18 @@ export async function dispatch(
60
68
  return chalk.yellow(`Unknown: /${cmd}. Type /help`)
61
69
  }
62
70
  }
71
+
72
+ async function handleUpdate(): Promise<string> {
73
+ const info = await checkForUpdate()
74
+ if (!info) return chalk.yellow("Could not check for updates.")
75
+ if (!info.hasUpdate) return chalk.green(`✓ Already up to date (v${info.current})`)
76
+
77
+ console.log(chalk.yellow(`\n⚡ Updating novacode to v${info.latest}...`))
78
+ const success = await runUpdate(true)
79
+ if (success) {
80
+ return chalk.green(
81
+ `✓ Successfully updated to v${info.latest}! Please restart nova to apply changes.`,
82
+ )
83
+ }
84
+ return chalk.red("✗ Update failed. Please try running 'nova update' manually in your terminal.")
85
+ }
package/src/main.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env bun
2
+ import { join } from "node:path"
3
+ import { parseArgs } from "node:util"
2
4
  /**
3
5
  * Entry point for the nova CLI.
4
6
  * Handles configuration, CLI flags, and switches between interactive/print modes.
5
7
  */
6
- import { parseArgs } from "node:util"
8
+ import chalk from "chalk"
7
9
  import { Agent } from "./agent/agent.ts"
8
10
  import { buildSystemPrompt } from "./agent/prompt.ts"
9
11
  import { handleSessionCommand } from "./commands/session.ts"
@@ -12,7 +14,7 @@ import { configExists, loadAuth, loadConfig } from "./config/store.ts"
12
14
  import { runOnboarding } from "./onboarding/wizard.ts"
13
15
  import { getSessionStore } from "./session/store.ts"
14
16
  import { getAllTools } from "./tools/index.ts"
15
- import { runPrintMode } from "./tui/print.ts"
17
+ import { runUpdate } from "./update.ts"
16
18
 
17
19
  // Ensure providers are registered
18
20
  import "./provider/openai.ts"
@@ -28,7 +30,7 @@ function parseCli() {
28
30
  "api-key": { type: "string" },
29
31
  session: { type: "string", short: "s" },
30
32
  },
31
- strict: false,
33
+ strict: true,
32
34
  allowPositionals: true,
33
35
  })
34
36
 
@@ -46,7 +48,7 @@ async function main() {
46
48
  const { flags, args } = parseCli()
47
49
 
48
50
  if (flags.version) {
49
- const pkg = await Bun.file("package.json").json()
51
+ const pkg = await Bun.file(join(import.meta.dir, "..", "package.json")).json()
50
52
  console.log(`nova ${pkg.version}`)
51
53
  process.exit(0)
52
54
  }
@@ -56,7 +58,7 @@ async function main() {
56
58
 
57
59
  Usage:
58
60
  nova Interactive mode
59
- nova "prompt" Print mode (non-interactive)
61
+ nova update Update to latest version
60
62
  nova session <cmd> Session management (list, delete)
61
63
  nova --session <id> Resume a session
62
64
 
@@ -76,6 +78,19 @@ Options:
76
78
  return
77
79
  }
78
80
 
81
+ // Handle update subcommand
82
+ if (args[0] === "update") {
83
+ await runUpdate()
84
+ return
85
+ }
86
+
87
+ // Reject positional args — use interactive mode with / commands
88
+ if (args.length > 0) {
89
+ console.error(chalk.yellow(`Unknown command: ${args.join(" ")}`))
90
+ console.error("Run `nova --help` for usage.")
91
+ process.exit(1)
92
+ }
93
+
79
94
  const controller = new AbortController()
80
95
 
81
96
  const onSignal = () => {
@@ -147,17 +162,7 @@ Options:
147
162
  messages: existingMessages,
148
163
  })
149
164
 
150
- // Print mode: prompt provided as arg
151
- const prompt = args.join(" ")
152
- if (prompt) {
153
- const result = await runPrintMode(agent, prompt, controller.signal)
154
- if (result) {
155
- store.appendMany(sessionId, result)
156
- }
157
- return
158
- }
159
-
160
- // Interactive TUI mode (Phase 3)
165
+ // Interactive TUI mode
161
166
  process.off("SIGINT", onSignal)
162
167
  process.off("SIGTERM", onSignal)
163
168
  const { interactive } = await import("./tui/app.tsx")
package/src/tui/app.tsx CHANGED
@@ -5,6 +5,7 @@ import type { Agent } from "../agent/agent.ts"
5
5
  import { COMMANDS, dispatch } from "../commands/index.ts"
6
6
  import type { SessionStore } from "../session/store.ts"
7
7
  import type { Msg } from "../types.ts"
8
+ import { checkForUpdate } from "../update.ts"
8
9
  import { formatToolArgs, makeRelative } from "../util.ts"
9
10
  import { formatMarkdown } from "./markdown.ts"
10
11
  export async function interactive(
@@ -71,6 +72,17 @@ function App({
71
72
  const history = useRef<string[]>([])
72
73
  const hIdx = useRef(-1)
73
74
  const abortCtrl = useRef<AbortController | null>(null)
75
+ const [updateInfo, setUpdateInfo] = useState<{ current: string; latest: string } | null>(null)
76
+
77
+ useEffect(() => {
78
+ const check = async () => {
79
+ const info = await checkForUpdate()
80
+ if (info?.hasUpdate) {
81
+ setUpdateInfo({ current: info.current, latest: info.latest })
82
+ }
83
+ }
84
+ check()
85
+ }, [])
74
86
 
75
87
  const isTypingCmd = input.startsWith("/") && !input.includes(" ")
76
88
  const suggestions = isTypingCmd
@@ -349,6 +361,23 @@ function App({
349
361
 
350
362
  {/* Input & Footer (Live) */}
351
363
  <Box flexDirection="column" marginTop={visibleMsgs.length > 0 || isLiveActive ? 1 : 0}>
364
+ {updateInfo && (
365
+ <Box
366
+ borderStyle="round"
367
+ borderColor="yellow"
368
+ paddingX={1}
369
+ marginBottom={1}
370
+ flexDirection="column"
371
+ >
372
+ <Text color="yellow" bold>
373
+ ⬆ Update Available (v{updateInfo.current} → v{updateInfo.latest})
374
+ </Text>
375
+ <Text dimColor>
376
+ Run <Text color="cyan">/update</Text> or <Text color="cyan">nova update</Text> to
377
+ upgrade.
378
+ </Text>
379
+ </Box>
380
+ )}
352
381
  <Box flexDirection="row">
353
382
  <Box flexShrink={0} marginRight={1}>
354
383
  <Text bold color="green">
package/src/update.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { join } from "node:path"
2
+ import { semver } from "bun"
3
+
4
+ let cachedLatest: string | null = null
5
+
6
+ export async function getCurrentVersion(): Promise<string> {
7
+ try {
8
+ const pkg = await Bun.file(join(import.meta.dir, "..", "package.json")).json()
9
+ return pkg.version ?? "0.0.0"
10
+ } catch {
11
+ return "0.0.0"
12
+ }
13
+ }
14
+
15
+ export async function getLatestVersion(): Promise<string | null> {
16
+ if (cachedLatest) return cachedLatest
17
+ try {
18
+ const proc = Bun.spawn(["bun", "info", "novacode", "version"], {
19
+ stdout: "pipe",
20
+ stderr: "ignore",
21
+ })
22
+ const text = await new Response(proc.stdout).text()
23
+ const latest = text.trim()
24
+ if (latest) {
25
+ cachedLatest = latest
26
+ return latest
27
+ }
28
+ } catch {}
29
+ return null
30
+ }
31
+
32
+ export async function checkForUpdate(): Promise<{
33
+ hasUpdate: boolean
34
+ current: string
35
+ latest: string
36
+ } | null> {
37
+ const current = await getCurrentVersion()
38
+ const latest = await getLatestVersion()
39
+ if (!latest) return null
40
+ return {
41
+ hasUpdate: semver.order(latest, current) === 1,
42
+ current,
43
+ latest,
44
+ }
45
+ }
46
+
47
+ export async function runUpdate(silent = false): Promise<boolean> {
48
+ const proc = Bun.spawn(["bun", "update", "-g", "novacode", "--latest"], {
49
+ stdout: silent ? "ignore" : "inherit",
50
+ stderr: silent ? "ignore" : "inherit",
51
+ })
52
+ const exitCode = await proc.exited
53
+ if (exitCode === 0) {
54
+ if (!silent) {
55
+ console.log("✓ novacode updated to latest version successfully.")
56
+ }
57
+ return true
58
+ } else {
59
+ if (!silent) {
60
+ console.error(`Update failed (exit code ${exitCode})`)
61
+ process.exit(1)
62
+ }
63
+ return false
64
+ }
65
+ }
package/src/tui/print.ts DELETED
@@ -1,75 +0,0 @@
1
- import chalk from "chalk"
2
- import type { Agent } from "../agent/agent.ts"
3
- import type { Msg } from "../types.ts"
4
- import { formatToolArgs } from "../util.ts"
5
- import { MarkdownRenderer } from "./markdown.ts"
6
-
7
- const TOOL_STYLE: Record<string, (s: string) => string> = {
8
- read: (s) => chalk.blue.bold(s),
9
- write: (s) => chalk.magenta.bold(s),
10
- edit: (s) => chalk.yellow.bold(s),
11
- bash: (s) => chalk.cyan.bold(s),
12
- glob: (s) => chalk.green.bold(s),
13
- find: (s) => chalk.green.bold(s),
14
- grep: (s) => chalk.green.bold(s),
15
- }
16
-
17
- function stylizeTool(name: string): string {
18
- const stylize = TOOL_STYLE[name] || ((s) => chalk.white.bold(s))
19
- return stylize(name)
20
- }
21
-
22
- export async function runPrintMode(
23
- agent: Agent,
24
- prompt: string,
25
- signal?: AbortSignal,
26
- ): Promise<Msg[] | undefined> {
27
- const stream = agent.prompt(prompt, signal)
28
- let output = ""
29
- let lastEventWasTool = false
30
-
31
- const renderer = new MarkdownRenderer()
32
- let lineBuffer = ""
33
-
34
- for await (const event of stream) {
35
- if (signal?.aborted) break
36
- if (event.type === "text_delta") {
37
- output += event.text
38
- lineBuffer += event.text
39
-
40
- if (lineBuffer.includes("\n")) {
41
- const lines = lineBuffer.split("\n")
42
- lineBuffer = lines.pop() ?? ""
43
- for (const line of lines) {
44
- process.stdout.write(`${renderer.renderLine(line)}\n`)
45
- }
46
- }
47
- lastEventWasTool = false
48
- }
49
- if (event.type === "tool_call") {
50
- const argsObj = event.call.args
51
- const argsStr = argsObj ? ` ${formatToolArgs(argsObj, true)}` : ""
52
- if (!lastEventWasTool) {
53
- process.stderr.write("\n")
54
- }
55
- process.stderr.write(`⏳ ${stylizeTool(event.call.name)}${argsStr}… `)
56
- lastEventWasTool = true
57
- }
58
- if (event.type === "tool_result") {
59
- const status = event.result.isError ? chalk.red("✗") : chalk.green("✓")
60
- const argsObj = event.result.args
61
- const argsStr = argsObj ? ` ${formatToolArgs(argsObj, true)}` : ""
62
- process.stderr.write(`\r${status} ${stylizeTool(event.result.tool)}${argsStr}\x1B[K\n`)
63
- }
64
- }
65
-
66
- if (lineBuffer) {
67
- process.stdout.write(renderer.renderLine(lineBuffer))
68
- }
69
-
70
- if (output && !output.endsWith("\n")) {
71
- process.stdout.write("\n")
72
- }
73
-
74
- return stream.result
75
- }