novacode 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novacode",
3
- "version": "0.4.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,10 +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 { join } from "node:path"
7
- import { parseArgs } from "node:util"
8
+ import chalk from "chalk"
8
9
  import { Agent } from "./agent/agent.ts"
9
10
  import { buildSystemPrompt } from "./agent/prompt.ts"
10
11
  import { handleSessionCommand } from "./commands/session.ts"
@@ -13,7 +14,7 @@ import { configExists, loadAuth, loadConfig } from "./config/store.ts"
13
14
  import { runOnboarding } from "./onboarding/wizard.ts"
14
15
  import { getSessionStore } from "./session/store.ts"
15
16
  import { getAllTools } from "./tools/index.ts"
16
- import { runPrintMode } from "./tui/print.ts"
17
+ import { runUpdate } from "./update.ts"
17
18
 
18
19
  // Ensure providers are registered
19
20
  import "./provider/openai.ts"
@@ -29,7 +30,7 @@ function parseCli() {
29
30
  "api-key": { type: "string" },
30
31
  session: { type: "string", short: "s" },
31
32
  },
32
- strict: false,
33
+ strict: true,
33
34
  allowPositionals: true,
34
35
  })
35
36
 
@@ -57,7 +58,7 @@ async function main() {
57
58
 
58
59
  Usage:
59
60
  nova Interactive mode
60
- nova "prompt" Print mode (non-interactive)
61
+ nova update Update to latest version
61
62
  nova session <cmd> Session management (list, delete)
62
63
  nova --session <id> Resume a session
63
64
 
@@ -77,6 +78,19 @@ Options:
77
78
  return
78
79
  }
79
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
+
80
94
  const controller = new AbortController()
81
95
 
82
96
  const onSignal = () => {
@@ -148,17 +162,7 @@ Options:
148
162
  messages: existingMessages,
149
163
  })
150
164
 
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)
165
+ // Interactive TUI mode
162
166
  process.off("SIGINT", onSignal)
163
167
  process.off("SIGTERM", onSignal)
164
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
- }