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 +13 -30
- package/package.json +1 -1
- package/src/commands/index.ts +23 -0
- package/src/main.ts +21 -16
- package/src/tui/app.tsx +29 -0
- package/src/update.ts +65 -0
- package/src/tui/print.ts +0 -75
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# NovaCode
|
|
2
2
|
|
|
3
|
-
Open-source, multi-provider coding agent.
|
|
3
|
+
Open-source, multi-provider coding agent.
|
|
4
4
|
|
|
5
5
|
> **Currently in early development.**
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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.
|
|
58
|
+
### 4. Flags & commands
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
nova
|
|
63
|
-
|
|
64
|
-
nova
|
|
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
package/src/commands/index.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
}
|