novacode 0.6.0 → 0.7.0

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.
@@ -1,25 +0,0 @@
1
- /**
2
- * TUI-specific static constants and configuration styles.
3
- */
4
-
5
- export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
6
-
7
- export const TOOL_STYLE: Record<string, string> = {
8
- read: "blue",
9
- write: "magenta",
10
- edit: "yellow",
11
- bash: "cyan",
12
- glob: "green",
13
- find: "green",
14
- grep: "green",
15
- tree: "green",
16
- }
17
-
18
- export const TERMINATION_PHRASES = [
19
- "Terminated by user",
20
- "Aborted by user",
21
- "Execution stopped",
22
- "Interrupted by user",
23
- "Agent halted",
24
- "Stopped by user",
25
- ]
@@ -1,62 +0,0 @@
1
- import chalk from "chalk"
2
-
3
- export class MarkdownRenderer {
4
- #inCodeBlock = false
5
- #codeBlockLang = ""
6
-
7
- renderLine(line: string): string {
8
- if (line.startsWith("```")) {
9
- if (this.#inCodeBlock) {
10
- this.#inCodeBlock = false
11
- return chalk.dim(`└${"─".repeat(50)}`)
12
- }
13
- this.#inCodeBlock = true
14
- this.#codeBlockLang = line.slice(3).trim()
15
- return chalk.dim(
16
- "┌" +
17
- "─".repeat(10) +
18
- ` [Code: ${this.#codeBlockLang || "text"}] ` +
19
- "─".repeat(40 - (this.#codeBlockLang?.length || 4)),
20
- )
21
- }
22
-
23
- if (this.#inCodeBlock) {
24
- return chalk.cyan(`│ ${line}`)
25
- }
26
-
27
- if (line.startsWith("#")) {
28
- const match = line.match(/^(#{1,6})\s+(.*)$/)
29
- if (match?.[1] && match[2]) {
30
- const level = match[1].length
31
- const content = match[2]
32
- if (level === 1) return chalk.bold.magenta.underline(content)
33
- if (level === 2) return chalk.bold.blue(content)
34
- return chalk.bold.cyan(content)
35
- }
36
- }
37
-
38
- let formatted = line
39
- if (formatted.startsWith("- ") || formatted.startsWith("* ")) {
40
- formatted = ` ${chalk.yellow("•")} ${formatted.slice(2)}`
41
- }
42
-
43
- formatted = formatted.replace(/`([^`]+)`/g, (_, code) => chalk.yellow(code))
44
- formatted = formatted.replace(/\*\*([^*]+)\*\*/g, (_, bold) => chalk.bold(bold))
45
- formatted = formatted.replace(/__([^_]+)__/g, (_, bold) => chalk.bold(bold))
46
- formatted = formatted.replace(/\*([^*]+)\*/g, (_, italic) => chalk.italic(italic))
47
- formatted = formatted.replace(/_([^_]+)_/g, (_, italic) => chalk.italic(italic))
48
- formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
49
- return `${chalk.blue(text)} ${chalk.dim(`(${url})`)}`
50
- })
51
-
52
- return formatted
53
- }
54
- }
55
-
56
- export function formatMarkdown(text: string): string {
57
- const renderer = new MarkdownRenderer()
58
- return text
59
- .split("\n")
60
- .map((line) => renderer.renderLine(line))
61
- .join("\n")
62
- }
@@ -1,205 +0,0 @@
1
- import { Box, render, Text, useInput } from "ink"
2
- import { useState } from "react"
3
-
4
- interface SelectOption {
5
- value: string
6
- label: string
7
- hint?: string
8
- }
9
-
10
- export function SelectPrompt({
11
- message,
12
- options,
13
- header,
14
- onSelect,
15
- }: {
16
- message: string
17
- options: SelectOption[]
18
- header?: string
19
- onSelect: (value: string | null) => void
20
- }) {
21
- const [idx, setIdx] = useState(0)
22
-
23
- useInput((_, key) => {
24
- if (key.escape) {
25
- onSelect(null)
26
- return
27
- }
28
- if (key.upArrow) {
29
- setIdx((i) => (i - 1 + options.length) % options.length)
30
- return
31
- }
32
- if (key.downArrow) {
33
- setIdx((i) => (i + 1) % options.length)
34
- return
35
- }
36
- if (key.return) {
37
- onSelect(options[idx]?.value ?? null)
38
- }
39
- })
40
-
41
- return (
42
- <Box flexDirection="column" paddingX={1}>
43
- {header && (
44
- <Box marginBottom={1}>
45
- <Text>{header}</Text>
46
- </Box>
47
- )}
48
- <Box marginBottom={1}>
49
- <Text bold>{message}</Text>
50
- </Box>
51
- {options.map((opt, i) => (
52
- <Box key={opt.value}>
53
- <Text color={i === idx ? "green" : undefined}>
54
- {i === idx ? "❯ " : " "}
55
- {opt.label}
56
- </Text>
57
- {opt.hint && i === idx && <Text dimColor> {opt.hint}</Text>}
58
- </Box>
59
- ))}
60
- <Box marginTop={1}>
61
- <Text dimColor>↑↓ navigate · Enter select · Esc cancel</Text>
62
- </Box>
63
- </Box>
64
- )
65
- }
66
-
67
- export function PasswordPrompt({
68
- message,
69
- validate,
70
- onSubmit,
71
- }: {
72
- message: string
73
- validate?: (v: string) => string | undefined
74
- onSubmit: (value: string | null) => void
75
- }) {
76
- const [value, setValue] = useState("")
77
- const [error, setError] = useState("")
78
-
79
- useInput((ch, key) => {
80
- if (key.escape) {
81
- onSubmit(null)
82
- return
83
- }
84
- if (key.return) {
85
- const err = validate?.(value)
86
- if (err) {
87
- setError(err)
88
- return
89
- }
90
- onSubmit(value)
91
- return
92
- }
93
- if (key.backspace || key.delete) {
94
- setValue((v) => v.slice(0, -1))
95
- setError("")
96
- return
97
- }
98
- if (ch) {
99
- setValue((v) => v + ch)
100
- setError("")
101
- }
102
- })
103
-
104
- return (
105
- <Box flexDirection="column" paddingX={1}>
106
- <Box marginBottom={1}>
107
- <Text bold>{message}</Text>
108
- </Box>
109
- <Box>
110
- <Text color="green">│ </Text>
111
- <Text dimColor>{"*".repeat(value.length)}</Text>
112
- <Text color="green">│</Text>
113
- </Box>
114
- {error && (
115
- <Box>
116
- <Text color="red">{error}</Text>
117
- </Box>
118
- )}
119
- <Box marginTop={1}>
120
- <Text dimColor>Enter submit · Esc cancel</Text>
121
- </Box>
122
- </Box>
123
- )
124
- }
125
-
126
- export function ConfirmPrompt({
127
- message,
128
- onConfirm,
129
- }: {
130
- message: string
131
- onConfirm: (value: boolean | null) => void
132
- }) {
133
- const [yes, setYes] = useState(true)
134
-
135
- useInput((_, key) => {
136
- if (key.escape) {
137
- onConfirm(null)
138
- return
139
- }
140
- if (key.leftArrow || key.rightArrow || key.tab) {
141
- setYes((y) => !y)
142
- return
143
- }
144
- if (key.return) {
145
- onConfirm(yes)
146
- }
147
- })
148
-
149
- return (
150
- <Box flexDirection="column" paddingX={1}>
151
- <Box marginBottom={1}>
152
- <Text bold>{message}</Text>
153
- </Box>
154
- <Box>
155
- <Text color={yes ? "green" : undefined}>{yes ? "❯ " : " "}Yes</Text>
156
- </Box>
157
- <Box>
158
- <Text color={!yes ? "red" : undefined}>{!yes ? "❯ " : " "}No</Text>
159
- </Box>
160
- <Box marginTop={1}>
161
- <Text dimColor>←→ toggle · Enter confirm · Esc cancel</Text>
162
- </Box>
163
- </Box>
164
- )
165
- }
166
-
167
- // Standalone wrappers for use outside the main TUI (e.g. onboarding)
168
-
169
- export function standaloneSelect(
170
- message: string,
171
- options: SelectOption[],
172
- header?: string,
173
- ): Promise<string | null> {
174
- return new Promise((resolve) => {
175
- const { unmount } = render(
176
- <SelectPrompt
177
- message={message}
178
- options={options}
179
- header={header}
180
- onSelect={(v) => {
181
- unmount()
182
- resolve(v)
183
- }}
184
- />,
185
- )
186
- })
187
- }
188
-
189
- export function standalonePassword(
190
- message: string,
191
- validate?: (v: string) => string | undefined,
192
- ): Promise<string | null> {
193
- return new Promise((resolve) => {
194
- const { unmount } = render(
195
- <PasswordPrompt
196
- message={message}
197
- validate={validate}
198
- onSubmit={(v) => {
199
- unmount()
200
- resolve(v)
201
- }}
202
- />,
203
- )
204
- })
205
- }
package/src/types.ts DELETED
@@ -1,262 +0,0 @@
1
- /**
2
- * Shared type definitions for the entire project.
3
- * Includes messaging, tools, providers, and agent loop events.
4
- */
5
- /** Content Parts */
6
-
7
- export interface TextPart {
8
- type: "text"
9
- text: string
10
- signature?: string
11
- }
12
-
13
- export interface ImagePart {
14
- type: "image"
15
- data: string // base64
16
- mime: string
17
- }
18
-
19
- export interface ThinkPart {
20
- type: "thinking"
21
- text: string
22
- signature?: string
23
- }
24
-
25
- export interface ToolCallPart {
26
- type: "tool_call"
27
- id: string
28
- name: string
29
- args: Record<string, unknown>
30
- signature?: string
31
- }
32
-
33
- export type ContentPart = TextPart | ImagePart | ThinkPart | ToolCallPart
34
-
35
- /** Messages */
36
-
37
- export interface UserMsg {
38
- role: "user"
39
- content: string | ContentPart[]
40
- ts: number
41
- }
42
-
43
- export interface AssistantMsg {
44
- role: "assistant"
45
- content: ContentPart[]
46
- model: string
47
- provider: string
48
- usage: Usage
49
- stop: StopReason
50
- error?: string
51
- ts: number
52
- }
53
-
54
- export interface ToolResultMsg {
55
- role: "tool_result"
56
- callId: string
57
- tool: string
58
- args?: Record<string, unknown>
59
- content: ContentPart[]
60
- isError: boolean
61
- ts: number
62
- }
63
-
64
- export type Msg = UserMsg | AssistantMsg | ToolResultMsg
65
- export type StopReason = "stop" | "length" | "tool_use" | "error" | "aborted"
66
-
67
- /** Usage */
68
-
69
- export interface Usage {
70
- in: number
71
- out: number
72
- cacheRead?: number
73
- cacheWrite?: number
74
- }
75
-
76
- /** Provider */
77
-
78
- export type ApiFormat = "openai" | "gemini"
79
-
80
- export interface ProviderDef {
81
- id: string
82
- name: string
83
- api: ApiFormat
84
- baseUrl: string
85
- envKey: string // env var name for API key
86
- }
87
-
88
- export interface Model {
89
- id: string
90
- name: string
91
- provider: string
92
- contextWindow: number
93
- maxTokens: number
94
- supportsThinking: boolean
95
- }
96
-
97
- /** Tools */
98
-
99
- export interface ToolDef {
100
- name: string
101
- description: string
102
- parameters: ToolParamDef
103
- }
104
-
105
- export interface ToolParamDef {
106
- type: "object"
107
- properties: Record<string, ToolPropDef>
108
- required?: string[]
109
- }
110
-
111
- export interface ToolPropDef {
112
- type: string
113
- description?: string
114
- enum?: string[]
115
- items?: ToolPropDef
116
- properties?: Record<string, ToolPropDef>
117
- required?: string[]
118
- }
119
-
120
- export interface ToolResult {
121
- content: ContentPart[]
122
- isError: boolean
123
- }
124
-
125
- export type ToolExecuteFn = (
126
- args: Record<string, unknown>,
127
- signal?: AbortSignal,
128
- ) => Promise<ToolResult>
129
-
130
- export interface Tool {
131
- def: ToolDef
132
- execute: ToolExecuteFn
133
- }
134
-
135
- /** Agent Events */
136
-
137
- export type AgentEvent =
138
- | { type: "start" }
139
- | { type: "turn" }
140
- | { type: "text_delta"; text: string }
141
- | { type: "thinking_delta"; text: string }
142
- | { type: "tool_call"; call: ToolCallPart }
143
- | { type: "assistant_msg"; msg: AssistantMsg }
144
- | { type: "tool_result"; callId: string; result: ToolResultMsg; args?: Record<string, unknown> }
145
- | { type: "turn_end"; msg: AssistantMsg; results: ToolResultMsg[] }
146
- | { type: "usage"; usage: Usage }
147
-
148
- /** Config */
149
-
150
- export interface NovaConfig {
151
- provider: string
152
- model: string
153
- }
154
-
155
- export interface NovaAuth {
156
- apiKeys: Record<string, string> // provider -> key
157
- }
158
-
159
- /** Session */
160
-
161
- export interface Session {
162
- id: string
163
- cwd: string
164
- model: string
165
- provider: string
166
- title: string | null
167
- created: number
168
- updated: number
169
- }
170
-
171
- export interface Compaction {
172
- summary: string
173
- seqBefore: number
174
- filesRead: string[]
175
- filesWrote: string[]
176
- ts: number
177
- }
178
-
179
- export interface CompactResult {
180
- compacted: boolean
181
- summary?: string
182
- msgsRemoved: number
183
- }
184
-
185
- /** Loop & Provider Types */
186
-
187
- export interface LoopCtx {
188
- system: string
189
- messages: Msg[]
190
- tools: Tool[]
191
- }
192
-
193
- export interface LoopOpts {
194
- api: ApiFormat
195
- model: Model
196
- apiKey: string
197
- baseUrl: string
198
- maxTurns?: number
199
- // Intercept tool calls before they execute
200
- beforeTool?: (
201
- call: ToolCallPart,
202
- args: Record<string, unknown>,
203
- ctx: LoopCtx,
204
- ) => Promise<{ block?: boolean; reason?: string } | undefined>
205
- // Run logic after a tool completes
206
- afterTool?: (call: ToolCallPart, result: ToolResultMsg, ctx: LoopCtx) => Promise<void>
207
- }
208
-
209
- export interface StreamOpts {
210
- api: ApiFormat
211
- model: Model
212
- apiKey: string
213
- baseUrl: string
214
- system: string
215
- messages: Msg[]
216
- tools: ToolDef[]
217
- signal?: AbortSignal
218
- }
219
-
220
- export interface IEventStream<T, R> {
221
- [Symbol.asyncIterator](): AsyncGenerator<T>
222
- result: R | undefined
223
- isDone: boolean
224
- }
225
-
226
- export type StreamFn = (opts: StreamOpts) => IEventStream<StreamEvent, AssistantResult>
227
-
228
- export interface StreamEvent {
229
- type: "text_delta" | "thinking_delta" | "tool_call" | "usage"
230
- text?: string
231
- call?: ToolCallPart
232
- usage?: Usage
233
- }
234
-
235
- export interface AssistantResult {
236
- content: ContentPart[]
237
- usage: Usage
238
- stop: StopReason
239
- }
240
-
241
- /** Prompts — used by interactive commands within the TUI */
242
-
243
- export interface Prompts {
244
- select(config: {
245
- message: string
246
- header?: string
247
- options: Array<{ value: string; label: string; hint?: string }>
248
- }): Promise<string | null>
249
- password(config: {
250
- message: string
251
- validate?: (v: string) => string | undefined
252
- }): Promise<string | null>
253
- confirm(config: { message: string }): Promise<boolean | null>
254
- }
255
-
256
- /** Commands */
257
-
258
- export interface Cmd {
259
- name: string
260
- desc: string
261
- aliases?: string[]
262
- }
package/src/update.ts DELETED
@@ -1,89 +0,0 @@
1
- import { spawn } from "node:child_process"
2
- import { readFile } from "node:fs/promises"
3
- import { dirname, join } from "node:path"
4
- import { fileURLToPath } from "node:url"
5
- import semver from "semver"
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url))
8
-
9
- let cachedLatest: string | null = null
10
- let cachedCurrent: string | null = null
11
-
12
- export async function getCurrentVersion(): Promise<string> {
13
- if (cachedCurrent) return cachedCurrent
14
- try {
15
- const raw = await readFile(join(__dirname, "..", "package.json"), "utf-8")
16
- const pkg = JSON.parse(raw)
17
- cachedCurrent = (pkg.version as string) ?? "0.0.0"
18
- return cachedCurrent
19
- } catch {
20
- return "0.0.0"
21
- }
22
- }
23
-
24
- export async function getLatestVersion(): Promise<string | null> {
25
- if (cachedLatest) return cachedLatest
26
- try {
27
- const proc = spawn("npm", ["info", "novacode", "version"], {
28
- stdio: ["ignore", "pipe", "ignore"],
29
- })
30
- const text = await new Promise<string>((resolve, reject) => {
31
- let out = ""
32
- proc.stdout.on("data", (chunk: Buffer) => {
33
- out += chunk.toString()
34
- })
35
- proc.on("error", reject)
36
- proc.on("close", () => resolve(out.trim()))
37
- })
38
- if (text) {
39
- cachedLatest = text
40
- return text
41
- }
42
- } catch {}
43
- return null
44
- }
45
-
46
- export async function checkForUpdate(): Promise<{
47
- hasUpdate: boolean
48
- current: string
49
- latest: string
50
- } | null> {
51
- const current = await getCurrentVersion()
52
- const latest = await getLatestVersion()
53
- if (!latest) return null
54
- return {
55
- hasUpdate: semver.gt(latest, current),
56
- current,
57
- latest,
58
- }
59
- }
60
-
61
- export async function runUpdate(silent = false): Promise<boolean> {
62
- try {
63
- const proc = spawn("npm", ["update", "-g", "novacode"], {
64
- stdio: silent ? "ignore" : "inherit",
65
- })
66
- const exitCode = await new Promise<number>((resolve, reject) => {
67
- proc.on("error", reject)
68
- proc.on("close", (code) => resolve(code ?? -1))
69
- })
70
- if (exitCode === 0) {
71
- if (!silent) {
72
- console.log("✓ novacode updated to latest version successfully.")
73
- }
74
- return true
75
- } else {
76
- if (!silent) {
77
- console.error(`Update failed (exit code ${exitCode})`)
78
- process.exit(1)
79
- }
80
- return false
81
- }
82
- } catch (e) {
83
- if (!silent) {
84
- console.error(`Update failed: ${(e as Error).message}`)
85
- process.exit(1)
86
- }
87
- return false
88
- }
89
- }
package/src/util.ts DELETED
@@ -1,80 +0,0 @@
1
- import { isAbsolute, relative } from "node:path"
2
- import chalk from "chalk"
3
- import type { Msg, TextPart } from "./types.ts"
4
-
5
- // ~4 chars per token for English/code. Close enough for capacity warnings.
6
- export function estimateTokens(messages: Msg[]): number {
7
- let chars = 0
8
- for (const msg of messages) {
9
- if (typeof msg.content === "string") {
10
- chars += msg.content.length
11
- } else if (Array.isArray(msg.content)) {
12
- for (const part of msg.content) {
13
- if (part.type === "text") chars += part.text.length
14
- }
15
- }
16
- }
17
- return Math.ceil(chars / 4)
18
- }
19
-
20
- export function textPart(s: string): TextPart {
21
- return { type: "text", text: s }
22
- }
23
-
24
- export function getRelativeIfInside(cwd: string, filePath: string): string {
25
- if (filePath === cwd || filePath.startsWith(`${cwd}/`)) {
26
- return relative(cwd, filePath) || "."
27
- }
28
- return filePath
29
- }
30
-
31
- export function makeRelative(val: string): string {
32
- if (typeof val !== "string") return val
33
-
34
- let pathVal = val
35
- let prefix = ""
36
- if (val.startsWith("file://")) {
37
- pathVal = val.slice(7)
38
- prefix = "file://"
39
- }
40
-
41
- if (isAbsolute(pathVal)) {
42
- const cwd = process.cwd()
43
- return prefix + getRelativeIfInside(cwd, pathVal)
44
- }
45
- return val
46
- }
47
-
48
- export function formatToolArgs(
49
- args: Record<string, unknown> | undefined,
50
- useChalk = false,
51
- ): string {
52
- if (!args) return ""
53
- return Object.entries(args)
54
- .map(([k, v]) => {
55
- const val = typeof v === "string" ? makeRelative(v) : JSON.stringify(v)
56
- const valStr = val.length > 40 ? `${val.slice(0, 40)}…` : val
57
- const keyStr = useChalk ? chalk.dim(`${k}:`) : `${k}:`
58
- return `${keyStr} ${valStr}`
59
- })
60
- .join(" ")
61
- }
62
-
63
- export function formatRelativeTime(ts: number): string {
64
- const now = Date.now()
65
- const diffMs = now - ts
66
- const diffSec = Math.floor(diffMs / 1000)
67
- const diffMin = Math.floor(diffSec / 60)
68
- const diffHour = Math.floor(diffMin / 60)
69
-
70
- if (diffSec < 60) {
71
- return "just now"
72
- }
73
- if (diffMin < 60) {
74
- return `${diffMin}m ago`
75
- }
76
- if (diffHour < 24) {
77
- return `${diffHour}h ago`
78
- }
79
- return new Date(ts).toLocaleDateString()
80
- }