novacode 0.5.2 → 0.5.3
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 +26 -2
- package/dist/app-BZ42XPxw.mjs +21 -0
- package/dist/app-BZ42XPxw.mjs.map +1 -0
- package/dist/main.mjs +110 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +25 -18
- package/src/agent/loop.ts +27 -8
- package/src/commands/index.ts +4 -3
- package/src/commands/models.ts +8 -9
- package/src/commands/providers.ts +63 -72
- package/src/config/providers.ts +12 -4
- package/src/config/store.ts +6 -7
- package/src/main.ts +7 -6
- package/src/onboarding/wizard.ts +26 -30
- package/src/provider/gemini.ts +12 -5
- package/src/provider/openai.ts +6 -9
- package/src/provider/stream.ts +63 -0
- package/src/session/compact.ts +1 -1
- package/src/session/store.ts +35 -35
- package/src/tools/fs.ts +10 -16
- package/src/tools/git.ts +26 -9
- package/src/tools/search.ts +33 -11
- package/src/tools/shell.ts +26 -25
- package/src/tui/app.tsx +132 -122
- package/src/tui/prompts.tsx +205 -0
- package/src/types.ts +15 -0
- package/src/update.ts +44 -23
- package/src/util.ts +1 -28
- package/src/provider/registry.ts +0 -62
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import * as clack from "@clack/prompts"
|
|
2
1
|
import chalk from "chalk"
|
|
3
2
|
import type { Agent } from "../agent/agent.ts"
|
|
4
3
|
import { getProvider, MODELS, PROVIDERS } from "../config/providers.ts"
|
|
5
4
|
import { loadAuth, loadConfig, saveAuth, saveConfig } from "../config/store.ts"
|
|
5
|
+
import type { Prompts } from "../types.ts"
|
|
6
|
+
|
|
7
|
+
export async function handleProviders(agent: Agent, prompts?: Prompts): Promise<string> {
|
|
8
|
+
if (!prompts) return chalk.red("Prompts not available in this context")
|
|
6
9
|
|
|
7
|
-
export async function handleProviders(agent: Agent): Promise<string> {
|
|
8
10
|
const config = await loadConfig()
|
|
9
11
|
const auth = await loadAuth()
|
|
10
|
-
|
|
11
12
|
const configured = PROVIDERS.filter((p) => !!auth.apiKeys[p.id])
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const act = await clack.select({
|
|
14
|
+
const headerLines =
|
|
15
|
+
configured.length === 0
|
|
16
|
+
? chalk.dim("No providers configured. Use 'Add Provider' below.")
|
|
17
|
+
: configured
|
|
18
|
+
.map((p) => {
|
|
19
|
+
const isDefault = p.id === config.provider
|
|
20
|
+
const active = isDefault ? chalk.green(" ●") : ""
|
|
21
|
+
const currentModel = isDefault
|
|
22
|
+
? config.model
|
|
23
|
+
: (MODELS.find((m) => m.provider === p.id)?.id ?? "")
|
|
24
|
+
return ` ✅ ${p.name.padEnd(24)} ${currentModel}${active}`
|
|
25
|
+
})
|
|
26
|
+
.join("\n")
|
|
27
|
+
|
|
28
|
+
const act = await prompts.select({
|
|
30
29
|
message: "Action",
|
|
30
|
+
header: headerLines,
|
|
31
31
|
options: [
|
|
32
32
|
{ value: "add", label: "Add Provider" },
|
|
33
33
|
{ value: "update", label: "Update API Key" },
|
|
@@ -36,16 +36,16 @@ export async function handleProviders(agent: Agent): Promise<string> {
|
|
|
36
36
|
{ value: "back", label: "Back" },
|
|
37
37
|
],
|
|
38
38
|
})
|
|
39
|
-
if (
|
|
39
|
+
if (!act || act === "back") return ""
|
|
40
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)
|
|
41
|
+
if (act === "add") return addProvider(agent, prompts)
|
|
42
|
+
if (act === "update") return updateKey(agent, prompts)
|
|
43
|
+
if (act === "remove") return removeKey(agent, prompts)
|
|
44
|
+
if (act === "default") return setDefault(agent, prompts)
|
|
45
45
|
return ""
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async function addProvider(agent: Agent): Promise<string> {
|
|
48
|
+
async function addProvider(agent: Agent, prompts: Prompts): Promise<string> {
|
|
49
49
|
const auth = await loadAuth()
|
|
50
50
|
const config = await loadConfig()
|
|
51
51
|
|
|
@@ -54,25 +54,24 @@ async function addProvider(agent: Agent): Promise<string> {
|
|
|
54
54
|
return chalk.yellow("All providers already have API keys configured.")
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const pick = await
|
|
57
|
+
const pick = await prompts.select({
|
|
58
58
|
message: "Add Provider",
|
|
59
59
|
options: available.map((p) => ({ value: p.id, label: p.name })),
|
|
60
60
|
})
|
|
61
|
-
if (
|
|
61
|
+
if (!pick) return ""
|
|
62
62
|
|
|
63
|
-
const pDef = getProvider(pick
|
|
63
|
+
const pDef = getProvider(pick)
|
|
64
64
|
if (!pDef) return chalk.red("Error: Provider not found")
|
|
65
65
|
|
|
66
|
-
const key = await
|
|
66
|
+
const key = await prompts.password({
|
|
67
67
|
message: `${pDef.name} API Key`,
|
|
68
68
|
validate: (v) => (!v || v.length < 8 ? "Enter a valid key" : undefined),
|
|
69
69
|
})
|
|
70
|
-
if (
|
|
70
|
+
if (!key) return ""
|
|
71
71
|
|
|
72
|
-
auth.apiKeys[pDef.id] = key
|
|
72
|
+
auth.apiKeys[pDef.id] = key
|
|
73
73
|
await saveAuth(auth)
|
|
74
74
|
|
|
75
|
-
// Set as active if no provider is currently set
|
|
76
75
|
if (!config.provider) {
|
|
77
76
|
config.provider = pDef.id
|
|
78
77
|
const mDef = MODELS.find((m) => m.provider === pDef.id)
|
|
@@ -83,7 +82,7 @@ async function addProvider(agent: Agent): Promise<string> {
|
|
|
83
82
|
agent.updateConfig({
|
|
84
83
|
api: pDef.api,
|
|
85
84
|
model: MODELS.find((m) => m.id === config.model)!,
|
|
86
|
-
apiKey: key
|
|
85
|
+
apiKey: key,
|
|
87
86
|
baseUrl: pDef.baseUrl,
|
|
88
87
|
})
|
|
89
88
|
}
|
|
@@ -91,7 +90,7 @@ async function addProvider(agent: Agent): Promise<string> {
|
|
|
91
90
|
return chalk.green(`✓ ${pDef.name} configured`)
|
|
92
91
|
}
|
|
93
92
|
|
|
94
|
-
async function updateKey(agent: Agent): Promise<string> {
|
|
93
|
+
async function updateKey(agent: Agent, prompts: Prompts): Promise<string> {
|
|
95
94
|
const auth = await loadAuth()
|
|
96
95
|
|
|
97
96
|
const configured = PROVIDERS.filter((p) => !!auth.apiKeys[p.id])
|
|
@@ -99,22 +98,21 @@ async function updateKey(agent: Agent): Promise<string> {
|
|
|
99
98
|
return chalk.yellow("No providers configured. Use 'Add Provider' first.")
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
const pick = await
|
|
101
|
+
const pick = await prompts.select({
|
|
103
102
|
message: "Update API Key",
|
|
104
103
|
options: configured.map((p) => ({ value: p.id, label: p.name })),
|
|
105
104
|
})
|
|
106
|
-
if (
|
|
105
|
+
if (!pick) return ""
|
|
107
106
|
|
|
108
|
-
const pDef = getProvider(pick
|
|
107
|
+
const pDef = getProvider(pick)
|
|
109
108
|
if (!pDef) return chalk.red("Error: Provider not found")
|
|
110
109
|
|
|
111
|
-
const key = await
|
|
112
|
-
if (
|
|
110
|
+
const key = await prompts.password({ message: `New key for ${pDef.name}` })
|
|
111
|
+
if (!key) return ""
|
|
113
112
|
|
|
114
|
-
auth.apiKeys[pDef.id] = key
|
|
113
|
+
auth.apiKeys[pDef.id] = key
|
|
115
114
|
await saveAuth(auth)
|
|
116
115
|
|
|
117
|
-
// If this is the active provider, update the agent's key
|
|
118
116
|
const config = await loadConfig()
|
|
119
117
|
if (config.provider === pDef.id) {
|
|
120
118
|
const currentModel = MODELS.find((m) => m.id === config.model && m.provider === config.provider)
|
|
@@ -122,7 +120,7 @@ async function updateKey(agent: Agent): Promise<string> {
|
|
|
122
120
|
agent.updateConfig({
|
|
123
121
|
api: pDef.api,
|
|
124
122
|
model: currentModel,
|
|
125
|
-
apiKey: key
|
|
123
|
+
apiKey: key,
|
|
126
124
|
baseUrl: pDef.baseUrl,
|
|
127
125
|
})
|
|
128
126
|
}
|
|
@@ -131,7 +129,7 @@ async function updateKey(agent: Agent): Promise<string> {
|
|
|
131
129
|
return chalk.green("✓ Key updated")
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
async function removeKey(agent: Agent): Promise<string> {
|
|
132
|
+
async function removeKey(agent: Agent, prompts: Prompts): Promise<string> {
|
|
135
133
|
const auth = await loadAuth()
|
|
136
134
|
const config = await loadConfig()
|
|
137
135
|
|
|
@@ -140,26 +138,23 @@ async function removeKey(agent: Agent): Promise<string> {
|
|
|
140
138
|
return chalk.yellow("No configured providers to remove.")
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
const pick = await
|
|
141
|
+
const pick = await prompts.select({
|
|
144
142
|
message: "Remove API Key",
|
|
145
143
|
options: configured.map((p) => ({ value: p.id, label: p.name })),
|
|
146
144
|
})
|
|
147
|
-
if (
|
|
145
|
+
if (!pick) return ""
|
|
148
146
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
message: `Are you sure you want to remove the API key for ${pId}?`,
|
|
147
|
+
const confirm = await prompts.confirm({
|
|
148
|
+
message: `Are you sure you want to remove the API key for ${pick}?`,
|
|
152
149
|
})
|
|
153
|
-
if (
|
|
150
|
+
if (!confirm) return ""
|
|
154
151
|
|
|
155
|
-
delete auth.apiKeys[
|
|
152
|
+
delete auth.apiKeys[pick]
|
|
156
153
|
await saveAuth(auth)
|
|
157
154
|
|
|
158
|
-
|
|
159
|
-
if (config.provider === pId) {
|
|
155
|
+
if (config.provider === pick) {
|
|
160
156
|
config.provider = ""
|
|
161
157
|
config.model = ""
|
|
162
|
-
// Try to find another configured provider
|
|
163
158
|
const next = Object.keys(auth.apiKeys)[0]
|
|
164
159
|
if (next) {
|
|
165
160
|
const pDef = getProvider(next)
|
|
@@ -178,43 +173,39 @@ async function removeKey(agent: Agent): Promise<string> {
|
|
|
178
173
|
await saveConfig(config)
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
return chalk.green(`✓ Removed API key for ${
|
|
176
|
+
return chalk.green(`✓ Removed API key for ${pick}`)
|
|
182
177
|
}
|
|
183
178
|
|
|
184
|
-
async function setDefault(agent: Agent): Promise<string> {
|
|
179
|
+
async function setDefault(agent: Agent, prompts: Prompts): Promise<string> {
|
|
185
180
|
const config = await loadConfig()
|
|
186
181
|
const auth = await loadAuth()
|
|
187
182
|
|
|
188
|
-
const pick = await
|
|
183
|
+
const pick = await prompts.select({
|
|
189
184
|
message: "Default Provider",
|
|
190
|
-
options: PROVIDERS.map((p) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
label: `${hasKey ? "✅" : "❌"} ${p.name}`,
|
|
195
|
-
}
|
|
196
|
-
}),
|
|
185
|
+
options: PROVIDERS.map((p) => ({
|
|
186
|
+
value: p.id,
|
|
187
|
+
label: `${auth.apiKeys[p.id] ? "✅" : "❌"} ${p.name}`,
|
|
188
|
+
})),
|
|
197
189
|
})
|
|
198
|
-
if (
|
|
190
|
+
if (!pick) return ""
|
|
199
191
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return chalk.yellow(`No API key for ${pId}. Please set one first.`)
|
|
192
|
+
if (!auth.apiKeys[pick]) {
|
|
193
|
+
return chalk.yellow(`No API key for ${pick}. Please set one first.`)
|
|
203
194
|
}
|
|
204
195
|
|
|
205
|
-
const pDef = getProvider(
|
|
206
|
-
const mDef = MODELS.find((m) => m.provider ===
|
|
196
|
+
const pDef = getProvider(pick)
|
|
197
|
+
const mDef = MODELS.find((m) => m.provider === pick)
|
|
207
198
|
|
|
208
199
|
if (!pDef || !mDef) return chalk.red("Error: Provider or model not found")
|
|
209
200
|
|
|
210
|
-
config.provider =
|
|
201
|
+
config.provider = pick
|
|
211
202
|
config.model = mDef.id
|
|
212
203
|
await saveConfig(config)
|
|
213
204
|
|
|
214
205
|
agent.updateConfig({
|
|
215
206
|
api: pDef.api,
|
|
216
207
|
model: mDef,
|
|
217
|
-
apiKey: auth.apiKeys[
|
|
208
|
+
apiKey: auth.apiKeys[pick],
|
|
218
209
|
baseUrl: pDef.baseUrl,
|
|
219
210
|
})
|
|
220
211
|
|
package/src/config/providers.ts
CHANGED
|
@@ -82,6 +82,14 @@ export const MODELS: Model[] = [
|
|
|
82
82
|
supportsThinking: false,
|
|
83
83
|
},
|
|
84
84
|
// Gemini
|
|
85
|
+
{
|
|
86
|
+
id: "gemini-3.5-flash",
|
|
87
|
+
name: "Gemini 3.5 Flash",
|
|
88
|
+
provider: "gemini",
|
|
89
|
+
contextWindow: 1_000_000,
|
|
90
|
+
maxTokens: 65_536,
|
|
91
|
+
supportsThinking: true,
|
|
92
|
+
},
|
|
85
93
|
{
|
|
86
94
|
id: "gemini-3.1-pro-preview",
|
|
87
95
|
name: "Gemini 3.1 Pro Preview",
|
|
@@ -128,7 +136,7 @@ export const MODELS: Model[] = [
|
|
|
128
136
|
provider: "gemini",
|
|
129
137
|
contextWindow: 2_000_000,
|
|
130
138
|
maxTokens: 65_536,
|
|
131
|
-
supportsThinking:
|
|
139
|
+
supportsThinking: false,
|
|
132
140
|
},
|
|
133
141
|
{
|
|
134
142
|
id: "gemini-2.5-flash",
|
|
@@ -136,7 +144,7 @@ export const MODELS: Model[] = [
|
|
|
136
144
|
provider: "gemini",
|
|
137
145
|
contextWindow: 1_000_000,
|
|
138
146
|
maxTokens: 65_536,
|
|
139
|
-
supportsThinking:
|
|
147
|
+
supportsThinking: false,
|
|
140
148
|
},
|
|
141
149
|
{
|
|
142
150
|
id: "gemini-2.5-flash-lite",
|
|
@@ -144,7 +152,7 @@ export const MODELS: Model[] = [
|
|
|
144
152
|
provider: "gemini",
|
|
145
153
|
contextWindow: 1_000_000,
|
|
146
154
|
maxTokens: 65_536,
|
|
147
|
-
supportsThinking:
|
|
155
|
+
supportsThinking: false,
|
|
148
156
|
},
|
|
149
157
|
{
|
|
150
158
|
id: "gemini-2.5-computer-use-preview-10-2025",
|
|
@@ -152,7 +160,7 @@ export const MODELS: Model[] = [
|
|
|
152
160
|
provider: "gemini",
|
|
153
161
|
contextWindow: 1_000_000,
|
|
154
162
|
maxTokens: 65_536,
|
|
155
|
-
supportsThinking:
|
|
163
|
+
supportsThinking: false,
|
|
156
164
|
},
|
|
157
165
|
// DeepSeek
|
|
158
166
|
{
|
package/src/config/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises"
|
|
1
2
|
import { join } from "node:path"
|
|
2
3
|
import type { NovaAuth, NovaConfig } from "../types.ts"
|
|
3
4
|
|
|
@@ -16,7 +17,7 @@ const defaultAuth: NovaAuth = {
|
|
|
16
17
|
|
|
17
18
|
export async function configExists(): Promise<boolean> {
|
|
18
19
|
try {
|
|
19
|
-
await
|
|
20
|
+
await stat(CONFIG_PATH())
|
|
20
21
|
return true
|
|
21
22
|
} catch {
|
|
22
23
|
return false
|
|
@@ -25,7 +26,7 @@ export async function configExists(): Promise<boolean> {
|
|
|
25
26
|
|
|
26
27
|
export async function loadConfig(): Promise<NovaConfig> {
|
|
27
28
|
try {
|
|
28
|
-
const raw = await
|
|
29
|
+
const raw = JSON.parse(await readFile(CONFIG_PATH(), "utf-8"))
|
|
29
30
|
return { ...defaultConfig, ...raw }
|
|
30
31
|
} catch {
|
|
31
32
|
return { ...defaultConfig }
|
|
@@ -34,7 +35,7 @@ export async function loadConfig(): Promise<NovaConfig> {
|
|
|
34
35
|
|
|
35
36
|
export async function loadAuth(): Promise<NovaAuth> {
|
|
36
37
|
try {
|
|
37
|
-
const raw = await
|
|
38
|
+
const raw = JSON.parse(await readFile(AUTH_PATH(), "utf-8"))
|
|
38
39
|
return { ...defaultAuth, ...raw }
|
|
39
40
|
} catch {
|
|
40
41
|
return { ...defaultAuth }
|
|
@@ -42,20 +43,18 @@ export async function loadAuth(): Promise<NovaAuth> {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
async function ensureDir(): Promise<void> {
|
|
45
|
-
const { mkdir } = await import("node:fs/promises")
|
|
46
46
|
await mkdir(NOVA_DIR(), { recursive: true })
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export async function saveConfig(config: NovaConfig): Promise<void> {
|
|
50
50
|
await ensureDir()
|
|
51
|
-
await
|
|
51
|
+
await writeFile(CONFIG_PATH(), JSON.stringify(config, null, 2))
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export async function saveAuth(auth: NovaAuth): Promise<void> {
|
|
55
55
|
await ensureDir()
|
|
56
|
-
await
|
|
56
|
+
await writeFile(AUTH_PATH(), JSON.stringify(auth, null, 2))
|
|
57
57
|
try {
|
|
58
|
-
const { chmod } = await import("node:fs/promises")
|
|
59
58
|
await chmod(AUTH_PATH(), 0o600)
|
|
60
59
|
} catch {
|
|
61
60
|
// chmod may fail on some platforms, non-fatal
|
package/src/main.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util"
|
|
3
3
|
/**
|
|
4
4
|
* Entry point for the nova CLI.
|
|
@@ -15,10 +15,6 @@ import { getSessionStore } from "./session/store.ts"
|
|
|
15
15
|
import { getAllTools } from "./tools/index.ts"
|
|
16
16
|
import { getCurrentVersion, runUpdate } from "./update.ts"
|
|
17
17
|
|
|
18
|
-
// Ensure providers are registered
|
|
19
|
-
import "./provider/openai.ts"
|
|
20
|
-
import "./provider/gemini.ts"
|
|
21
|
-
|
|
22
18
|
function parseCli() {
|
|
23
19
|
const { values, positionals } = parseArgs({
|
|
24
20
|
options: {
|
|
@@ -168,7 +164,12 @@ Options:
|
|
|
168
164
|
await interactive(agent, store, sessionId)
|
|
169
165
|
}
|
|
170
166
|
|
|
167
|
+
process.on("unhandledRejection", (reason) => {
|
|
168
|
+
console.error("Unhandled rejection:", reason)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
})
|
|
171
|
+
|
|
171
172
|
main().catch((e) => {
|
|
172
|
-
console.error("Fatal:", e
|
|
173
|
+
console.error("Fatal:", e)
|
|
173
174
|
process.exit(1)
|
|
174
175
|
})
|
package/src/onboarding/wizard.ts
CHANGED
|
@@ -1,58 +1,54 @@
|
|
|
1
|
-
import
|
|
1
|
+
import chalk from "chalk"
|
|
2
2
|
import { getModelsForProvider, getProvider, PROVIDERS } from "../config/providers.ts"
|
|
3
3
|
import { saveAuth, saveConfig } from "../config/store.ts"
|
|
4
|
+
import { standalonePassword, standaloneSelect } from "../tui/prompts.tsx"
|
|
4
5
|
import type { NovaConfig } from "../types.ts"
|
|
5
6
|
|
|
6
7
|
export async function runOnboarding(): Promise<NovaConfig> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const providerId = await
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
clack.cancel("Cancelled")
|
|
8
|
+
console.log(chalk.bold.cyan("\n⚡ Nova — your coding companion\n"))
|
|
9
|
+
|
|
10
|
+
const providerId = await standaloneSelect(
|
|
11
|
+
"Pick a provider",
|
|
12
|
+
PROVIDERS.map((p) => ({ value: p.id, label: p.name })),
|
|
13
|
+
)
|
|
14
|
+
if (!providerId) {
|
|
15
|
+
console.log(chalk.dim("Cancelled"))
|
|
16
16
|
process.exit(0)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const provider = getProvider(providerId
|
|
19
|
+
const provider = getProvider(providerId)
|
|
20
20
|
if (!provider) {
|
|
21
|
-
|
|
21
|
+
console.log(chalk.red("Unknown provider"))
|
|
22
22
|
process.exit(1)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const apiKey = await
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (clack.isCancel(apiKey)) {
|
|
30
|
-
clack.cancel("Cancelled")
|
|
25
|
+
const apiKey = await standalonePassword(`Enter ${provider.name} API key`)
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
console.log(chalk.dim("Cancelled"))
|
|
31
28
|
process.exit(0)
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
const models = getModelsForProvider(providerId
|
|
35
|
-
const modelId = await
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const models = getModelsForProvider(providerId)
|
|
32
|
+
const modelId = await standaloneSelect(
|
|
33
|
+
"Pick a default model",
|
|
34
|
+
models.map((m) => ({
|
|
38
35
|
value: m.id,
|
|
39
36
|
label: `${m.name} (${(m.contextWindow / 1000).toFixed(0)}k ctx)`,
|
|
40
37
|
})),
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
clack.cancel("Cancelled")
|
|
38
|
+
)
|
|
39
|
+
if (!modelId) {
|
|
40
|
+
console.log(chalk.dim("Cancelled"))
|
|
45
41
|
process.exit(0)
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
const config: NovaConfig = {
|
|
49
|
-
provider: providerId
|
|
50
|
-
model: modelId
|
|
45
|
+
provider: providerId,
|
|
46
|
+
model: modelId,
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
await saveConfig(config)
|
|
54
|
-
await saveAuth({ apiKeys: { [providerId
|
|
50
|
+
await saveAuth({ apiKeys: { [providerId]: apiKey } })
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
console.log(chalk.green("\n✓ Ready. Type your prompt or /help for commands\n"))
|
|
57
53
|
return config
|
|
58
54
|
}
|
package/src/provider/gemini.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
ToolDef,
|
|
10
10
|
Usage,
|
|
11
11
|
} from "../types.ts"
|
|
12
|
-
import { register } from "./registry.ts"
|
|
13
12
|
import { EventStream } from "./stream.ts"
|
|
14
13
|
|
|
15
14
|
interface GeminiPart {
|
|
@@ -200,10 +199,20 @@ export const streamGemini: StreamFn = (
|
|
|
200
199
|
if (part.thought === true || typeof part.thought === "string") {
|
|
201
200
|
const thoughtText = typeof part.thought === "string" ? part.thought : part.text
|
|
202
201
|
es.push({ type: "thinking_delta", text: thoughtText })
|
|
203
|
-
|
|
202
|
+
const last = content[content.length - 1]
|
|
203
|
+
if (last?.type === "thinking") {
|
|
204
|
+
last.text += thoughtText
|
|
205
|
+
} else {
|
|
206
|
+
content.push({ type: "thinking", text: thoughtText, signature: sig })
|
|
207
|
+
}
|
|
204
208
|
} else {
|
|
205
209
|
es.push({ type: "text_delta", text: part.text })
|
|
206
|
-
|
|
210
|
+
const last = content[content.length - 1]
|
|
211
|
+
if (last?.type === "text") {
|
|
212
|
+
last.text += part.text
|
|
213
|
+
} else {
|
|
214
|
+
content.push({ type: "text", text: part.text, signature: sig })
|
|
215
|
+
}
|
|
207
216
|
}
|
|
208
217
|
}
|
|
209
218
|
|
|
@@ -250,5 +259,3 @@ export const streamGemini: StreamFn = (
|
|
|
250
259
|
|
|
251
260
|
return es
|
|
252
261
|
}
|
|
253
|
-
|
|
254
|
-
register("gemini", streamGemini)
|
package/src/provider/openai.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AssistantResult,
|
|
3
|
-
ContentPart,
|
|
4
3
|
Msg,
|
|
5
4
|
StopReason,
|
|
6
5
|
StreamEvent,
|
|
@@ -9,8 +8,6 @@ import type {
|
|
|
9
8
|
ToolDef,
|
|
10
9
|
Usage,
|
|
11
10
|
} from "../types.ts"
|
|
12
|
-
import { consolidate } from "../util.ts"
|
|
13
|
-
import { register } from "./registry.ts"
|
|
14
11
|
import { EventStream } from "./stream.ts"
|
|
15
12
|
|
|
16
13
|
function msgToOpenAI(msg: Msg): Record<string, unknown> {
|
|
@@ -118,8 +115,8 @@ export const streamOpenAI: StreamFn = (
|
|
|
118
115
|
let buffer = ""
|
|
119
116
|
const currentToolCalls = new Map<number, { id: string; name: string; args: string }>()
|
|
120
117
|
let usage: Usage = { in: 0, out: 0 }
|
|
118
|
+
let textContent = ""
|
|
121
119
|
let stop = "stop"
|
|
122
|
-
const textParts: ContentPart[] = []
|
|
123
120
|
|
|
124
121
|
while (true) {
|
|
125
122
|
const { done, value } = await reader.read()
|
|
@@ -142,7 +139,7 @@ export const streamOpenAI: StreamFn = (
|
|
|
142
139
|
|
|
143
140
|
if (delta.content) {
|
|
144
141
|
es.push({ type: "text_delta", text: delta.content })
|
|
145
|
-
|
|
142
|
+
textContent += delta.content
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
if (delta.tool_calls) {
|
|
@@ -178,7 +175,10 @@ export const streamOpenAI: StreamFn = (
|
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
const content: AssistantResult["content"] =
|
|
178
|
+
const content: AssistantResult["content"] = []
|
|
179
|
+
if (textContent) {
|
|
180
|
+
content.push({ type: "text", text: textContent })
|
|
181
|
+
}
|
|
182
182
|
for (const [, tc] of currentToolCalls) {
|
|
183
183
|
content.push({
|
|
184
184
|
type: "tool_call",
|
|
@@ -213,6 +213,3 @@ export const streamOpenAI: StreamFn = (
|
|
|
213
213
|
|
|
214
214
|
return es
|
|
215
215
|
}
|
|
216
|
-
|
|
217
|
-
// Auto-register
|
|
218
|
-
register("openai", streamOpenAI)
|
package/src/provider/stream.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import type { AgentEvent, ApiFormat, AssistantResult, StreamFn, StreamOpts } from "../types.ts"
|
|
2
|
+
import { streamGemini } from "./gemini.ts"
|
|
3
|
+
import { streamOpenAI } from "./openai.ts"
|
|
4
|
+
|
|
5
|
+
export type { AssistantResult, StreamEvent, StreamFn, StreamOpts } from "../types.ts"
|
|
6
|
+
|
|
1
7
|
/*
|
|
2
8
|
* Push-based async event stream.
|
|
3
9
|
*
|
|
@@ -75,3 +81,60 @@ export class EventStream<T, R> {
|
|
|
75
81
|
return this.#done
|
|
76
82
|
}
|
|
77
83
|
}
|
|
84
|
+
|
|
85
|
+
// Internal map of registered provider implementations
|
|
86
|
+
const registry = new Map<ApiFormat, StreamFn>([
|
|
87
|
+
["openai", streamOpenAI],
|
|
88
|
+
["gemini", streamGemini],
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
export function register(api: ApiFormat, fn: StreamFn): void {
|
|
92
|
+
registry.set(api, fn)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Bridges provider-specific StreamEvents into AgentEvents so the loop and TUI deal with one type.
|
|
96
|
+
export function stream(opts: StreamOpts): EventStream<AgentEvent, AssistantResult> {
|
|
97
|
+
const fn = registry.get(opts.api)
|
|
98
|
+
if (!fn) throw new Error(`No provider registered for API format: ${opts.api}`)
|
|
99
|
+
|
|
100
|
+
// Bridge layer: converts provider-specific StreamEvents into the agent's
|
|
101
|
+
// AgentEvent shape, so the loop and TUI only deal with one event type.
|
|
102
|
+
const providerStream = fn(opts)
|
|
103
|
+
const agentStream = new EventStream<AgentEvent, AssistantResult>()
|
|
104
|
+
|
|
105
|
+
;(async () => {
|
|
106
|
+
for await (const event of providerStream) {
|
|
107
|
+
if (event.type === "text_delta") {
|
|
108
|
+
agentStream.push({ type: "text_delta", text: event.text ?? "" })
|
|
109
|
+
} else if (event.type === "thinking_delta") {
|
|
110
|
+
agentStream.push({ type: "thinking_delta", text: event.text ?? "" })
|
|
111
|
+
} else if (event.type === "tool_call" && event.call) {
|
|
112
|
+
agentStream.push({
|
|
113
|
+
type: "tool_call",
|
|
114
|
+
call: {
|
|
115
|
+
type: "tool_call",
|
|
116
|
+
id: event.call.id,
|
|
117
|
+
name: event.call.name,
|
|
118
|
+
args: event.call.args,
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
} else if (event.type === "usage" && event.usage) {
|
|
122
|
+
agentStream.push({ type: "usage", usage: event.usage })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const res = providerStream.result
|
|
127
|
+
if (res) {
|
|
128
|
+
agentStream.finish(res)
|
|
129
|
+
} else {
|
|
130
|
+
// Fallback for unexpected closure
|
|
131
|
+
agentStream.finish({ content: [], usage: { in: 0, out: 0 }, stop: "stop" })
|
|
132
|
+
}
|
|
133
|
+
})()
|
|
134
|
+
|
|
135
|
+
return agentStream
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getRegisteredApis(): ApiFormat[] {
|
|
139
|
+
return [...registry.keys()]
|
|
140
|
+
}
|
package/src/session/compact.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getProvider } from "../config/providers.ts"
|
|
2
|
-
import { stream } from "../provider/
|
|
2
|
+
import { stream } from "../provider/stream.ts"
|
|
3
3
|
import type { Model, Msg } from "../types.ts"
|
|
4
4
|
import { estimateTokens } from "../util.ts"
|
|
5
5
|
import type { SessionStore } from "./store.ts"
|