novacode 0.2.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.
@@ -0,0 +1,28 @@
1
+ import chalk from "chalk"
2
+ import type { Agent } from "../agent/agent.ts"
3
+ import { compact as runCompact } from "../session/compact.ts"
4
+ import type { SessionStore } from "../session/store.ts"
5
+
6
+ export async function handleCompact(
7
+ agent: Agent,
8
+ store: SessionStore,
9
+ sessionId: string,
10
+ ): Promise<string> {
11
+ const res = await runCompact(
12
+ store,
13
+ sessionId,
14
+ agent.messages,
15
+ agent.model,
16
+ agent.apiKey,
17
+ agent.baseUrl,
18
+ )
19
+
20
+ if (res.compacted) {
21
+ // Update agent messages
22
+ const msgs = store.messages(sessionId)
23
+ agent.setMessages(msgs)
24
+ return chalk.green(`✓ Context compacted (${res.msgsRemoved} messages removed)`)
25
+ }
26
+
27
+ return chalk.yellow("Context is small enough, no compaction needed.")
28
+ }
@@ -0,0 +1,62 @@
1
+ import chalk from "chalk"
2
+ import type { Agent } from "../agent/agent.ts"
3
+ import type { SessionStore } from "../session/store.ts"
4
+ import type { Cmd } from "../types.ts"
5
+ import { handleCompact } from "./compact.ts"
6
+ import { handleModels } from "./models.ts"
7
+ import { handleProviders } from "./providers.ts"
8
+
9
+ export const COMMANDS: Cmd[] = [
10
+ { name: "models", desc: "Switch model", aliases: ["model"] },
11
+ { name: "providers", desc: "Manage providers", aliases: ["prov", "config", "cfg"] },
12
+ { name: "compact", desc: "Compact context" },
13
+ { name: "help", desc: "Show help" },
14
+ { name: "clear", desc: "Clear screen" },
15
+ { name: "quit", desc: "Exit (Ctrl+D)", aliases: ["exit"] },
16
+ ]
17
+
18
+ const HELP = `
19
+ ${chalk.bold("Commands:")}
20
+ ${COMMANDS.map((c) => ` /${c.name.padEnd(12)} ${c.desc}`).join("\n")}
21
+
22
+ ${chalk.dim("Keys:")}
23
+ Esc Abort
24
+ ↑ / ↓ History
25
+ `
26
+
27
+ export async function dispatch(
28
+ input: string,
29
+ agent: Agent,
30
+ store?: SessionStore,
31
+ sessionId?: string,
32
+ ): Promise<string | null> {
33
+ const [cmd, ...rest] = input.slice(1).split(" ")
34
+ const args = rest.join(" ")
35
+
36
+ switch (cmd) {
37
+ case "models":
38
+ case "model":
39
+ return handleModels(args, agent)
40
+ case "providers":
41
+ case "prov":
42
+ case "config":
43
+ case "cfg":
44
+ return handleProviders(agent)
45
+ case "compact":
46
+ if (!store || !sessionId) return chalk.red("Session store not available")
47
+ return handleCompact(agent, store, sessionId)
48
+ case "help":
49
+ return HELP
50
+ case "clear":
51
+ console.clear()
52
+ return ""
53
+ case "quit":
54
+ process.exit(0)
55
+ return null
56
+ case "exit":
57
+ process.exit(0)
58
+ return null
59
+ default:
60
+ return chalk.yellow(`Unknown: /${cmd}. Type /help`)
61
+ }
62
+ }
@@ -0,0 +1,86 @@
1
+ import * as clack from "@clack/prompts"
2
+ import chalk from "chalk"
3
+ import type { Agent } from "../agent/agent.ts"
4
+ import { getProvider, MODELS } from "../config/providers.ts"
5
+ import { loadAuth, loadConfig, saveConfig } from "../config/store.ts"
6
+
7
+ export async function handleModels(args: string, agent: Agent): Promise<string> {
8
+ const config = await loadConfig()
9
+ const auth = await loadAuth()
10
+
11
+ if (args) return await switchDirect(args.trim(), agent)
12
+
13
+ const options: clack.Option<string>[] = []
14
+ for (const m of MODELS) {
15
+ const cur = m.id === config.model && m.provider === config.provider
16
+ const pDef = getProvider(m.provider)
17
+ if (!pDef) continue
18
+
19
+ // Ensure we have an API key for the provider
20
+ const hasKey = !!auth.apiKeys[m.provider]
21
+ if (!hasKey) continue
22
+
23
+ options.push({
24
+ value: `${m.provider}:${m.id}`,
25
+ label: `${cur ? chalk.green("●") : "○"} ${m.id.padEnd(20)} ${fmt(m.contextWindow).padEnd(8)}`,
26
+ hint: pDef.name,
27
+ })
28
+ }
29
+
30
+ if (!options.length)
31
+ return chalk.yellow("No models available. Use /providers to add a provider API key.")
32
+
33
+ const pick = await clack.select({ message: "Model", options })
34
+ if (clack.isCancel(pick)) return ""
35
+
36
+ const [pk, mid] = (pick as string).split(":")
37
+ const selectedModel = MODELS.find((m) => m.provider === pk && m.id === mid)
38
+ const selectedProvider = getProvider(pk!)
39
+
40
+ if (!selectedModel || !selectedProvider) return chalk.red("Error: Model or provider not found")
41
+
42
+ config.provider = pk!
43
+ config.model = mid!
44
+ await saveConfig(config)
45
+
46
+ // Update agent
47
+ agent.updateConfig({
48
+ api: selectedProvider.api,
49
+ model: selectedModel,
50
+ apiKey: auth.apiKeys[pk!] ?? "",
51
+ baseUrl: selectedProvider.baseUrl,
52
+ })
53
+ return chalk.green(`✓ Switched to ${mid}`)
54
+ }
55
+
56
+ async function switchDirect(id: string, agent: Agent): Promise<string> {
57
+ const config = await loadConfig()
58
+ const auth = await loadAuth()
59
+
60
+ const m = MODELS.find((m) => m.id === id)
61
+ if (!m) return chalk.yellow(`"${id}" not found. Use /models`)
62
+
63
+ const pk = m.provider
64
+ if (!auth.apiKeys[pk]) {
65
+ return chalk.yellow(`No API key configured for ${pk}. Use /providers`)
66
+ }
67
+
68
+ const selectedProvider = getProvider(pk)
69
+ if (!selectedProvider) return chalk.red("Error: Provider not found")
70
+
71
+ config.provider = pk
72
+ config.model = id
73
+ await saveConfig(config)
74
+
75
+ // Update agent
76
+ agent.updateConfig({
77
+ api: selectedProvider.api,
78
+ model: m,
79
+ apiKey: auth.apiKeys[pk],
80
+ baseUrl: selectedProvider.baseUrl,
81
+ })
82
+
83
+ return chalk.green(`✓ Switched to ${id}`)
84
+ }
85
+
86
+ const fmt = (n: number) => (n >= 1_000_000 ? `${n / 1_000_000}M` : `${n / 1000}K`)
@@ -0,0 +1,222 @@
1
+ import * as clack from "@clack/prompts"
2
+ import chalk from "chalk"
3
+ import type { Agent } from "../agent/agent.ts"
4
+ import { getProvider, MODELS, PROVIDERS } from "../config/providers.ts"
5
+ import { loadAuth, loadConfig, saveAuth, saveConfig } from "../config/store.ts"
6
+
7
+ export async function handleProviders(agent: Agent): Promise<string> {
8
+ const config = await loadConfig()
9
+ const auth = await loadAuth()
10
+
11
+ const configured = PROVIDERS.filter((p) => !!auth.apiKeys[p.id])
12
+
13
+ console.log(chalk.bold("\n ⚙ Configured Providers:\n"))
14
+ if (configured.length === 0) {
15
+ console.log(chalk.dim(" No providers configured. Use 'Add Provider' below.\n"))
16
+ } else {
17
+ for (const p of configured) {
18
+ const isDefault = p.id === config.provider
19
+ const active = isDefault ? chalk.green(" ●") : ""
20
+ const key = chalk.green("✅")
21
+ const currentModel = isDefault
22
+ ? config.model
23
+ : (MODELS.find((m) => m.provider === p.id)?.id ?? "")
24
+ console.log(` ${key} ${p.name.padEnd(24)} ${currentModel}${active}`)
25
+ }
26
+ console.log("") // Spacer
27
+ }
28
+
29
+ const act = await clack.select({
30
+ message: "Action",
31
+ options: [
32
+ { value: "add", label: "Add Provider" },
33
+ { value: "update", label: "Update API Key" },
34
+ { value: "remove", label: "Remove API Key" },
35
+ { value: "default", label: "Set Default Provider" },
36
+ { value: "back", label: "Back" },
37
+ ],
38
+ })
39
+ if (clack.isCancel(act) || act === "back") return ""
40
+
41
+ if (act === "add") return addProvider(agent)
42
+ if (act === "update") return updateKey(agent)
43
+ if (act === "remove") return removeKey(agent)
44
+ if (act === "default") return setDefault(agent)
45
+ return ""
46
+ }
47
+
48
+ async function addProvider(agent: Agent): Promise<string> {
49
+ const auth = await loadAuth()
50
+ const config = await loadConfig()
51
+
52
+ const available = PROVIDERS.filter((p) => !auth.apiKeys[p.id])
53
+ if (available.length === 0) {
54
+ return chalk.yellow("All providers already have API keys configured.")
55
+ }
56
+
57
+ const pick = await clack.select({
58
+ message: "Add Provider",
59
+ options: available.map((p) => ({ value: p.id, label: p.name })),
60
+ })
61
+ if (clack.isCancel(pick)) return ""
62
+
63
+ const pDef = getProvider(pick as string)
64
+ if (!pDef) return chalk.red("Error: Provider not found")
65
+
66
+ const key = await clack.password({
67
+ message: `${pDef.name} API Key`,
68
+ validate: (v) => (!v || v.length < 8 ? "Enter a valid key" : undefined),
69
+ })
70
+ if (clack.isCancel(key)) return ""
71
+
72
+ auth.apiKeys[pDef.id] = key as string
73
+ await saveAuth(auth)
74
+
75
+ // Set as active if no provider is currently set
76
+ if (!config.provider) {
77
+ config.provider = pDef.id
78
+ const mDef = MODELS.find((m) => m.provider === pDef.id)
79
+ if (mDef) {
80
+ config.model = mDef.id
81
+ }
82
+ await saveConfig(config)
83
+ agent.updateConfig({
84
+ api: pDef.api,
85
+ model: MODELS.find((m) => m.id === config.model)!,
86
+ apiKey: key as string,
87
+ baseUrl: pDef.baseUrl,
88
+ })
89
+ }
90
+
91
+ return chalk.green(`✓ ${pDef.name} configured`)
92
+ }
93
+
94
+ async function updateKey(agent: Agent): Promise<string> {
95
+ const auth = await loadAuth()
96
+
97
+ const configured = PROVIDERS.filter((p) => !!auth.apiKeys[p.id])
98
+ if (configured.length === 0) {
99
+ return chalk.yellow("No providers configured. Use 'Add Provider' first.")
100
+ }
101
+
102
+ const pick = await clack.select({
103
+ message: "Update API Key",
104
+ options: configured.map((p) => ({ value: p.id, label: p.name })),
105
+ })
106
+ if (clack.isCancel(pick)) return ""
107
+
108
+ const pDef = getProvider(pick as string)
109
+ if (!pDef) return chalk.red("Error: Provider not found")
110
+
111
+ const key = await clack.password({ message: `New key for ${pDef.name}` })
112
+ if (clack.isCancel(key)) return ""
113
+
114
+ auth.apiKeys[pDef.id] = key as string
115
+ await saveAuth(auth)
116
+
117
+ // If this is the active provider, update the agent's key
118
+ const config = await loadConfig()
119
+ if (config.provider === pDef.id) {
120
+ const currentModel = MODELS.find((m) => m.id === config.model && m.provider === config.provider)
121
+ if (currentModel) {
122
+ agent.updateConfig({
123
+ api: pDef.api,
124
+ model: currentModel,
125
+ apiKey: key as string,
126
+ baseUrl: pDef.baseUrl,
127
+ })
128
+ }
129
+ }
130
+
131
+ return chalk.green("✓ Key updated")
132
+ }
133
+
134
+ async function removeKey(agent: Agent): Promise<string> {
135
+ const auth = await loadAuth()
136
+ const config = await loadConfig()
137
+
138
+ const configured = PROVIDERS.filter((p) => !!auth.apiKeys[p.id])
139
+ if (configured.length === 0) {
140
+ return chalk.yellow("No configured providers to remove.")
141
+ }
142
+
143
+ const pick = await clack.select({
144
+ message: "Remove API Key",
145
+ options: configured.map((p) => ({ value: p.id, label: p.name })),
146
+ })
147
+ if (clack.isCancel(pick)) return ""
148
+
149
+ const pId = pick as string
150
+ const confirm = await clack.confirm({
151
+ message: `Are you sure you want to remove the API key for ${pId}?`,
152
+ })
153
+ if (clack.isCancel(confirm) || !confirm) return ""
154
+
155
+ delete auth.apiKeys[pId]
156
+ await saveAuth(auth)
157
+
158
+ // If removing the active provider's key
159
+ if (config.provider === pId) {
160
+ config.provider = ""
161
+ config.model = ""
162
+ // Try to find another configured provider
163
+ const next = Object.keys(auth.apiKeys)[0]
164
+ if (next) {
165
+ const pDef = getProvider(next)
166
+ const mDef = MODELS.find((m) => m.provider === next)
167
+ if (pDef && mDef) {
168
+ config.provider = next
169
+ config.model = mDef.id
170
+ agent.updateConfig({
171
+ api: pDef.api,
172
+ model: mDef,
173
+ apiKey: auth.apiKeys[next]!,
174
+ baseUrl: pDef.baseUrl,
175
+ })
176
+ }
177
+ }
178
+ await saveConfig(config)
179
+ }
180
+
181
+ return chalk.green(`✓ Removed API key for ${pId}`)
182
+ }
183
+
184
+ async function setDefault(agent: Agent): Promise<string> {
185
+ const config = await loadConfig()
186
+ const auth = await loadAuth()
187
+
188
+ const pick = await clack.select({
189
+ message: "Default Provider",
190
+ options: PROVIDERS.map((p) => {
191
+ const hasKey = !!auth.apiKeys[p.id]
192
+ return {
193
+ value: p.id,
194
+ label: `${hasKey ? "✅" : "❌"} ${p.name}`,
195
+ }
196
+ }),
197
+ })
198
+ if (clack.isCancel(pick)) return ""
199
+
200
+ const pId = pick as string
201
+ if (!auth.apiKeys[pId]) {
202
+ return chalk.yellow(`No API key for ${pId}. Please set one first.`)
203
+ }
204
+
205
+ const pDef = getProvider(pId)
206
+ const mDef = MODELS.find((m) => m.provider === pId)
207
+
208
+ if (!pDef || !mDef) return chalk.red("Error: Provider or model not found")
209
+
210
+ config.provider = pId
211
+ config.model = mDef.id
212
+ await saveConfig(config)
213
+
214
+ agent.updateConfig({
215
+ api: pDef.api,
216
+ model: mDef,
217
+ apiKey: auth.apiKeys[pId],
218
+ baseUrl: pDef.baseUrl,
219
+ })
220
+
221
+ return chalk.green(`✓ Default set to ${pDef.name} (${mDef.id})`)
222
+ }
@@ -0,0 +1,40 @@
1
+ import { getSessionStore } from "../session/store.ts"
2
+
3
+ export async function handleSessionCommand(args: string[]): Promise<void> {
4
+ const store = getSessionStore()
5
+ const [subcommand, id] = args
6
+
7
+ if (subcommand === "list" || subcommand === "ls") {
8
+ const sessions = store.list()
9
+ if (sessions.length === 0) {
10
+ console.log("No sessions found.")
11
+ return
12
+ }
13
+
14
+ console.log("ID".padEnd(25), "MODEL".padEnd(20), "UPDATED")
15
+ console.log("-".repeat(70))
16
+ for (const s of sessions) {
17
+ const date = new Date(s.updated).toLocaleString()
18
+ console.log(s.id.padEnd(25), s.model.padEnd(20), date)
19
+ }
20
+ return
21
+ }
22
+
23
+ if (subcommand === "delete" || subcommand === "rm") {
24
+ if (!id) {
25
+ console.error("Usage: novacode session delete <id>")
26
+ process.exit(1)
27
+ }
28
+ const success = store.delete(id)
29
+ if (success) {
30
+ console.log(`Deleted session: ${id}`)
31
+ } else {
32
+ console.error(`Session not found: ${id}`)
33
+ process.exit(1)
34
+ }
35
+ return
36
+ }
37
+
38
+ console.error("Unknown session subcommand. Use 'list' or 'delete'.")
39
+ process.exit(1)
40
+ }
@@ -0,0 +1,199 @@
1
+ import type { Model, ProviderDef } from "../types.ts"
2
+
3
+ export const PROVIDERS: ProviderDef[] = [
4
+ {
5
+ id: "glm",
6
+ name: "GLM (Z.AI)",
7
+ api: "openai",
8
+ baseUrl: "https://api.z.ai/api/coding/paas/v4",
9
+ envKey: "GLM_API_KEY",
10
+ },
11
+ {
12
+ id: "gemini",
13
+ name: "Gemini (Google)",
14
+ api: "gemini",
15
+ baseUrl: "https://generativelanguage.googleapis.com",
16
+ envKey: "GEMINI_API_KEY",
17
+ },
18
+ {
19
+ id: "deepseek",
20
+ name: "DeepSeek",
21
+ api: "openai",
22
+ baseUrl: "https://api.deepseek.com",
23
+ envKey: "DEEPSEEK_API_KEY",
24
+ },
25
+ {
26
+ id: "openai",
27
+ name: "OpenAI",
28
+ api: "openai",
29
+ baseUrl: "https://api.openai.com/v1",
30
+ envKey: "OPENAI_API_KEY",
31
+ },
32
+ ]
33
+
34
+ export const MODELS: Model[] = [
35
+ // GLM
36
+ {
37
+ id: "glm-5.1",
38
+ name: "GLM-5.1",
39
+ provider: "glm",
40
+ contextWindow: 128_000,
41
+ maxTokens: 4096,
42
+ supportsThinking: false,
43
+ },
44
+ {
45
+ id: "glm-5",
46
+ name: "GLM-5",
47
+ provider: "glm",
48
+ contextWindow: 128_000,
49
+ maxTokens: 4096,
50
+ supportsThinking: false,
51
+ },
52
+ {
53
+ id: "glm-5-turbo",
54
+ name: "GLM-5 Turbo",
55
+ provider: "glm",
56
+ contextWindow: 128_000,
57
+ maxTokens: 4096,
58
+ supportsThinking: false,
59
+ },
60
+ {
61
+ id: "glm-4.7",
62
+ name: "GLM-4.7",
63
+ provider: "glm",
64
+ contextWindow: 128_000,
65
+ maxTokens: 4096,
66
+ supportsThinking: false,
67
+ },
68
+ {
69
+ id: "glm-4.7-flash",
70
+ name: "GLM-4.7 Flash (Free)",
71
+ provider: "glm",
72
+ contextWindow: 128_000,
73
+ maxTokens: 4096,
74
+ supportsThinking: false,
75
+ },
76
+ {
77
+ id: "glm-4.5-flash",
78
+ name: "GLM-4.5 Flash (Free)",
79
+ provider: "glm",
80
+ contextWindow: 128_000,
81
+ maxTokens: 4096,
82
+ supportsThinking: false,
83
+ },
84
+ // Gemini
85
+ {
86
+ id: "gemini-3.1-pro-preview",
87
+ name: "Gemini 3.1 Pro Preview",
88
+ provider: "gemini",
89
+ contextWindow: 2_000_000,
90
+ maxTokens: 65_536,
91
+ supportsThinking: true,
92
+ },
93
+ {
94
+ id: "gemini-3.1-pro-preview-customtools",
95
+ name: "Gemini 3.1 Pro (Custom Tools)",
96
+ provider: "gemini",
97
+ contextWindow: 2_000_000,
98
+ maxTokens: 65_536,
99
+ supportsThinking: true,
100
+ },
101
+ {
102
+ id: "gemini-3.1-flash-lite",
103
+ name: "Gemini 3.1 Flash-Lite",
104
+ provider: "gemini",
105
+ contextWindow: 1_000_000,
106
+ maxTokens: 65_536,
107
+ supportsThinking: true,
108
+ },
109
+ {
110
+ id: "gemini-3.1-flash-lite-preview",
111
+ name: "Gemini 3.1 Flash-Lite Preview",
112
+ provider: "gemini",
113
+ contextWindow: 1_000_000,
114
+ maxTokens: 65_536,
115
+ supportsThinking: true,
116
+ },
117
+ {
118
+ id: "gemini-3-flash-preview",
119
+ name: "Gemini 3 Flash Preview",
120
+ provider: "gemini",
121
+ contextWindow: 1_000_000,
122
+ maxTokens: 65_536,
123
+ supportsThinking: true,
124
+ },
125
+ {
126
+ id: "gemini-2.5-pro",
127
+ name: "Gemini 2.5 Pro",
128
+ provider: "gemini",
129
+ contextWindow: 2_000_000,
130
+ maxTokens: 65_536,
131
+ supportsThinking: true,
132
+ },
133
+ {
134
+ id: "gemini-2.5-flash",
135
+ name: "Gemini 2.5 Flash",
136
+ provider: "gemini",
137
+ contextWindow: 1_000_000,
138
+ maxTokens: 65_536,
139
+ supportsThinking: true,
140
+ },
141
+ {
142
+ id: "gemini-2.5-flash-lite",
143
+ name: "Gemini 2.5 Flash-Lite",
144
+ provider: "gemini",
145
+ contextWindow: 1_000_000,
146
+ maxTokens: 65_536,
147
+ supportsThinking: true,
148
+ },
149
+ {
150
+ id: "gemini-2.5-computer-use-preview-10-2025",
151
+ name: "Gemini 2.5 Computer Use",
152
+ provider: "gemini",
153
+ contextWindow: 1_000_000,
154
+ maxTokens: 65_536,
155
+ supportsThinking: true,
156
+ },
157
+ // DeepSeek
158
+ {
159
+ id: "deepseek-chat",
160
+ name: "DeepSeek V3",
161
+ provider: "deepseek",
162
+ contextWindow: 64_000,
163
+ maxTokens: 8_192,
164
+ supportsThinking: false,
165
+ },
166
+ {
167
+ id: "deepseek-reasoner",
168
+ name: "DeepSeek R1",
169
+ provider: "deepseek",
170
+ contextWindow: 64_000,
171
+ maxTokens: 8_192,
172
+ supportsThinking: true,
173
+ },
174
+ // OpenAI
175
+ {
176
+ id: "gpt-4o",
177
+ name: "GPT-4o",
178
+ provider: "openai",
179
+ contextWindow: 128_000,
180
+ maxTokens: 16_384,
181
+ supportsThinking: false,
182
+ },
183
+ {
184
+ id: "o4-mini",
185
+ name: "o4-mini",
186
+ provider: "openai",
187
+ contextWindow: 200_000,
188
+ maxTokens: 100_000,
189
+ supportsThinking: true,
190
+ },
191
+ ]
192
+
193
+ export function getProvider(id: string): ProviderDef | undefined {
194
+ return PROVIDERS.find((p) => p.id === id)
195
+ }
196
+
197
+ export function getModelsForProvider(providerId: string): Model[] {
198
+ return MODELS.filter((m) => m.provider === providerId)
199
+ }
@@ -0,0 +1,67 @@
1
+ import { join } from "node:path"
2
+ import type { NovaAuth, NovaConfig } from "../types.ts"
3
+
4
+ const NOVA_DIR = () => join(process.env.HOME ?? "~", ".novacode")
5
+ const CONFIG_PATH = () => join(NOVA_DIR(), "config.json")
6
+ const AUTH_PATH = () => join(NOVA_DIR(), "auth.json")
7
+
8
+ const defaultConfig: NovaConfig = {
9
+ provider: "",
10
+ model: "",
11
+ }
12
+
13
+ const defaultAuth: NovaAuth = {
14
+ apiKeys: {},
15
+ }
16
+
17
+ export async function configExists(): Promise<boolean> {
18
+ try {
19
+ await Bun.file(CONFIG_PATH()).stat()
20
+ return true
21
+ } catch {
22
+ return false
23
+ }
24
+ }
25
+
26
+ export async function loadConfig(): Promise<NovaConfig> {
27
+ try {
28
+ const raw = await Bun.file(CONFIG_PATH()).json()
29
+ return { ...defaultConfig, ...raw }
30
+ } catch {
31
+ return { ...defaultConfig }
32
+ }
33
+ }
34
+
35
+ export async function loadAuth(): Promise<NovaAuth> {
36
+ try {
37
+ const raw = await Bun.file(AUTH_PATH()).json()
38
+ return { ...defaultAuth, ...raw }
39
+ } catch {
40
+ return { ...defaultAuth }
41
+ }
42
+ }
43
+
44
+ async function ensureDir(): Promise<void> {
45
+ const { mkdir } = await import("node:fs/promises")
46
+ await mkdir(NOVA_DIR(), { recursive: true })
47
+ }
48
+
49
+ export async function saveConfig(config: NovaConfig): Promise<void> {
50
+ await ensureDir()
51
+ await Bun.write(CONFIG_PATH(), JSON.stringify(config, null, 2))
52
+ }
53
+
54
+ export async function saveAuth(auth: NovaAuth): Promise<void> {
55
+ await ensureDir()
56
+ await Bun.write(AUTH_PATH(), JSON.stringify(auth, null, 2))
57
+ try {
58
+ const { chmod } = await import("node:fs/promises")
59
+ await chmod(AUTH_PATH(), 0o600)
60
+ } catch {
61
+ // chmod may fail on some platforms, non-fatal
62
+ }
63
+ }
64
+
65
+ export function getNovaDir(): string {
66
+ return NOVA_DIR()
67
+ }