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 +2 -9
- package/package.json +6 -4
- package/src/commands/index.ts +23 -0
- package/src/main.ts +22 -19
- package/src/tui/app.tsx +31 -1
- package/src/update.ts +68 -0
- package/src/tui/print.ts +0 -75
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.
|
|
41
|
+
### 3. Start chatting
|
|
42
42
|
|
|
43
|
-
|
|
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.
|
|
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": "
|
|
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"
|
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,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
|
|
5
|
+
* Handles configuration, CLI flags, and runs interactive TUI mode.
|
|
5
6
|
*/
|
|
6
|
-
import
|
|
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 {
|
|
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:
|
|
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
|
|
51
|
-
console.log(`nova ${
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
}
|