novacode 0.4.0 → 0.5.2

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
@@ -38,9 +38,9 @@ On first launch, nova walks you through a quick setup:
38
38
 
39
39
  That's it. You're ready to go.
40
40
 
41
- ### 3. Two ways to use it
41
+ ### 3. Start chatting
42
42
 
43
- **Interactive mode** — just run `nova` and chat:
43
+ Just run `nova` to start chatting:
44
44
 
45
45
  ```bash
46
46
  nova
@@ -48,13 +48,6 @@ nova
48
48
 
49
49
  You'll get a prompt where you can ask questions, give coding tasks, and use `/help` for available commands.
50
50
 
51
- **Print mode** — pass a prompt as an argument (non-interactive, streams output to stdout):
52
-
53
- ```bash
54
- nova "explain the auth module in this project"
55
- nova "fix the type error in src/utils.ts"
56
- ```
57
-
58
51
  ### 4. Flags & commands
59
52
 
60
53
  Available flags: `--provider`, `--model`, `--api-key`, `-s` (resume session)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novacode",
3
- "version": "0.4.0",
3
+ "version": "0.5.2",
4
4
  "description": "Open-source multi-provider coding agent. Bun-native.",
5
5
  "type": "module",
6
6
  "module": "src/main.ts",
@@ -20,8 +20,7 @@
20
20
  "format": "biome format --write .",
21
21
  "typecheck": "tsc --noEmit",
22
22
  "build": "bun build src/main.ts --compile --outfile nova",
23
- "check": "bun run typecheck && bun run lint && bun test",
24
- "prepublishOnly": "bun run check"
23
+ "check": "bun run typecheck && bun run lint && bun test"
25
24
  },
26
25
  "keywords": [
27
26
  "coding-agent",
@@ -30,7 +29,10 @@
30
29
  "bun",
31
30
  "llm"
32
31
  ],
33
- "license": "MIT",
32
+ "license": "Apache-2.0",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
34
36
  "repository": {
35
37
  "type": "git",
36
38
  "url": "https://github.com/rwitesh/novacode"
@@ -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,10 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ import { parseArgs } from "node:util"
2
3
  /**
3
4
  * Entry point for the nova CLI.
4
- * Handles configuration, CLI flags, and switches between interactive/print modes.
5
+ * Handles configuration, CLI flags, and runs interactive TUI mode.
5
6
  */
6
- import { join } from "node:path"
7
- import { parseArgs } from "node:util"
7
+ import chalk from "chalk"
8
8
  import { Agent } from "./agent/agent.ts"
9
9
  import { buildSystemPrompt } from "./agent/prompt.ts"
10
10
  import { handleSessionCommand } from "./commands/session.ts"
@@ -13,7 +13,7 @@ import { configExists, loadAuth, loadConfig } from "./config/store.ts"
13
13
  import { runOnboarding } from "./onboarding/wizard.ts"
14
14
  import { getSessionStore } from "./session/store.ts"
15
15
  import { getAllTools } from "./tools/index.ts"
16
- import { runPrintMode } from "./tui/print.ts"
16
+ import { getCurrentVersion, runUpdate } from "./update.ts"
17
17
 
18
18
  // Ensure providers are registered
19
19
  import "./provider/openai.ts"
@@ -29,7 +29,7 @@ function parseCli() {
29
29
  "api-key": { type: "string" },
30
30
  session: { type: "string", short: "s" },
31
31
  },
32
- strict: false,
32
+ strict: true,
33
33
  allowPositionals: true,
34
34
  })
35
35
 
@@ -47,8 +47,8 @@ async function main() {
47
47
  const { flags, args } = parseCli()
48
48
 
49
49
  if (flags.version) {
50
- const pkg = await Bun.file(join(import.meta.dir, "..", "package.json")).json()
51
- console.log(`nova ${pkg.version}`)
50
+ const version = await getCurrentVersion()
51
+ console.log(`nova ${version}`)
52
52
  process.exit(0)
53
53
  }
54
54
 
@@ -57,7 +57,7 @@ async function main() {
57
57
 
58
58
  Usage:
59
59
  nova Interactive mode
60
- nova "prompt" Print mode (non-interactive)
60
+ nova update Update to latest version
61
61
  nova session <cmd> Session management (list, delete)
62
62
  nova --session <id> Resume a session
63
63
 
@@ -77,6 +77,19 @@ Options:
77
77
  return
78
78
  }
79
79
 
80
+ // Handle update subcommand
81
+ if (args[0] === "update") {
82
+ await runUpdate()
83
+ return
84
+ }
85
+
86
+ // Reject positional args — use interactive mode with / commands
87
+ if (args.length > 0) {
88
+ console.error(chalk.yellow(`Unknown command: ${args.join(" ")}`))
89
+ console.error("Run `nova --help` for usage.")
90
+ process.exit(1)
91
+ }
92
+
80
93
  const controller = new AbortController()
81
94
 
82
95
  const onSignal = () => {
@@ -148,17 +161,7 @@ Options:
148
161
  messages: existingMessages,
149
162
  })
150
163
 
151
- // Print mode: prompt provided as arg
152
- const prompt = args.join(" ")
153
- if (prompt) {
154
- const result = await runPrintMode(agent, prompt, controller.signal)
155
- if (result) {
156
- store.appendMany(sessionId, result)
157
- }
158
- return
159
- }
160
-
161
- // Interactive TUI mode (Phase 3)
164
+ // Interactive TUI mode
162
165
  process.off("SIGINT", onSignal)
163
166
  process.off("SIGTERM", onSignal)
164
167
  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, getCurrentVersion } from "../update.ts"
8
9
  import { formatToolArgs, makeRelative } from "../util.ts"
9
10
  import { formatMarkdown } from "./markdown.ts"
10
11
  export async function interactive(
@@ -14,7 +15,8 @@ export async function interactive(
14
15
  ): Promise<void> {
15
16
  // Hide system cursor during session
16
17
  process.stdout.write("\x1B[?25l")
17
- process.stdout.write(chalk.bold.cyan("⚡ novacode\n"))
18
+ const version = await getCurrentVersion()
19
+ process.stdout.write(`${chalk.cyan.bold("⚡ novacode")} ${chalk.gray(`v${version}`)}\n`)
18
20
 
19
21
  try {
20
22
  const { waitUntilExit } = render(<App agent={agent} store={store} sessionId={sessionId} />)
@@ -71,6 +73,17 @@ function App({
71
73
  const history = useRef<string[]>([])
72
74
  const hIdx = useRef(-1)
73
75
  const abortCtrl = useRef<AbortController | null>(null)
76
+ const [updateInfo, setUpdateInfo] = useState<{ current: string; latest: string } | null>(null)
77
+
78
+ useEffect(() => {
79
+ const check = async () => {
80
+ const info = await checkForUpdate()
81
+ if (info?.hasUpdate) {
82
+ setUpdateInfo({ current: info.current, latest: info.latest })
83
+ }
84
+ }
85
+ check()
86
+ }, [])
74
87
 
75
88
  const isTypingCmd = input.startsWith("/") && !input.includes(" ")
76
89
  const suggestions = isTypingCmd
@@ -349,6 +362,23 @@ function App({
349
362
 
350
363
  {/* Input & Footer (Live) */}
351
364
  <Box flexDirection="column" marginTop={visibleMsgs.length > 0 || isLiveActive ? 1 : 0}>
365
+ {updateInfo && (
366
+ <Box
367
+ borderStyle="round"
368
+ borderColor="yellow"
369
+ paddingX={1}
370
+ marginBottom={1}
371
+ flexDirection="column"
372
+ >
373
+ <Text color="yellow" bold>
374
+ ⬆ Update Available (v{updateInfo.current} → v{updateInfo.latest})
375
+ </Text>
376
+ <Text dimColor>
377
+ Run <Text color="cyan">/update</Text> or <Text color="cyan">nova update</Text> to
378
+ upgrade.
379
+ </Text>
380
+ </Box>
381
+ )}
352
382
  <Box flexDirection="row">
353
383
  <Box flexShrink={0} marginRight={1}>
354
384
  <Text bold color="green">
package/src/update.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { join } from "node:path"
2
+ import { semver } from "bun"
3
+
4
+ let cachedLatest: string | null = null
5
+ let cachedCurrent: string | null = null
6
+
7
+ export async function getCurrentVersion(): Promise<string> {
8
+ if (cachedCurrent) return cachedCurrent
9
+ try {
10
+ const pkg = await Bun.file(join(import.meta.dir, "..", "package.json")).json()
11
+ cachedCurrent = (pkg.version as string) ?? "0.0.0"
12
+ return cachedCurrent
13
+ } catch {
14
+ return "0.0.0"
15
+ }
16
+ }
17
+
18
+ export async function getLatestVersion(): Promise<string | null> {
19
+ if (cachedLatest) return cachedLatest
20
+ try {
21
+ const proc = Bun.spawn(["bun", "info", "novacode", "version"], {
22
+ stdout: "pipe",
23
+ stderr: "ignore",
24
+ })
25
+ const text = await new Response(proc.stdout).text()
26
+ const latest = text.trim()
27
+ if (latest) {
28
+ cachedLatest = latest
29
+ return latest
30
+ }
31
+ } catch {}
32
+ return null
33
+ }
34
+
35
+ export async function checkForUpdate(): Promise<{
36
+ hasUpdate: boolean
37
+ current: string
38
+ latest: string
39
+ } | null> {
40
+ const current = await getCurrentVersion()
41
+ const latest = await getLatestVersion()
42
+ if (!latest) return null
43
+ return {
44
+ hasUpdate: semver.order(latest, current) === 1,
45
+ current,
46
+ latest,
47
+ }
48
+ }
49
+
50
+ export async function runUpdate(silent = false): Promise<boolean> {
51
+ const proc = Bun.spawn(["bun", "update", "-g", "novacode", "--latest"], {
52
+ stdout: silent ? "ignore" : "inherit",
53
+ stderr: silent ? "ignore" : "inherit",
54
+ })
55
+ const exitCode = await proc.exited
56
+ if (exitCode === 0) {
57
+ if (!silent) {
58
+ console.log("✓ novacode updated to latest version successfully.")
59
+ }
60
+ return true
61
+ } else {
62
+ if (!silent) {
63
+ console.error(`Update failed (exit code ${exitCode})`)
64
+ process.exit(1)
65
+ }
66
+ return false
67
+ }
68
+ }
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
- }