opencode-ghost 0.3.0 → 0.4.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.
- package/CHANGELOG.md +13 -1
- package/package.json +2 -2
- package/src/stateless.ts +92 -11
- package/src/tui.tsx +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Expanded direct stateless suggestion generation: resolve API keys directly from OpenCode provider state (`api.state.provider`), add default endpoints for well-known providers (OpenAI, DeepSeek, Groq, Mistral, OpenRouter, Together, Ollama, Perplexity, xAI, Fireworks, Cerebras), and support keyless local completions without creating temporary OpenCode sessions.
|
|
15
|
+
## [0.3.1] - 2026-09-28
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **install:** Dedupe tui plugin entry across relative and absolute paths
|
|
10
20
|
## [0.3.0] - 2026-09-28
|
|
11
21
|
|
|
12
22
|
### Added
|
|
@@ -127,7 +137,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
127
137
|
- Options for `model`, `acceptKeys`, `maxChars`, `idleDelayMs`,
|
|
128
138
|
`recentMessages` and `system`.
|
|
129
139
|
|
|
130
|
-
[Unreleased]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.
|
|
140
|
+
[Unreleased]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.4.0...HEAD
|
|
141
|
+
[0.4.0]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.3.1...v0.4.0
|
|
142
|
+
[0.3.1]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.3.0...v0.3.1
|
|
131
143
|
[0.3.0]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.7...v0.3.0
|
|
132
144
|
[0.2.7]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.6...v0.2.7
|
|
133
145
|
[0.2.6]: https://github.com/ozandogrultan/opencode-ghost/compare/v0.2.5...v0.2.6
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ghost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Claude Code-style next-prompt suggestions in the opencode TUI, shown as dimmed ghost text and accepted with Tab. An opencode plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
"@opentui/solid": "0.4.5",
|
|
64
64
|
"@types/node": "24.12.2",
|
|
65
65
|
"husky": "^9.1.7",
|
|
66
|
-
"typescript": "
|
|
66
|
+
"typescript": "7.0.2"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/stateless.ts
CHANGED
|
@@ -3,6 +3,34 @@ import * as os from "node:os"
|
|
|
3
3
|
import * as path from "node:path"
|
|
4
4
|
import type { ResolvedOptions } from "./options"
|
|
5
5
|
|
|
6
|
+
const WELL_KNOWN_BASE_URLS: Record<string, string> = {
|
|
7
|
+
openai: "https://api.openai.com/v1",
|
|
8
|
+
deepseek: "https://api.deepseek.com",
|
|
9
|
+
groq: "https://api.groq.com/openai/v1",
|
|
10
|
+
mistral: "https://api.mistral.com/v1",
|
|
11
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
12
|
+
together: "https://api.together.xyz/v1",
|
|
13
|
+
ollama: "http://127.0.0.1:11434/v1",
|
|
14
|
+
perplexity: "https://api.perplexity.ai",
|
|
15
|
+
xai: "https://api.x.ai/v1",
|
|
16
|
+
fireworks: "https://api.fireworks.ai/inference/v1",
|
|
17
|
+
cerebras: "https://api.cerebras.ai/v1",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const WELL_KNOWN_ENV_KEYS: Record<string, string[]> = {
|
|
21
|
+
openai: ["OPENAI_API_KEY"],
|
|
22
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
23
|
+
groq: ["GROQ_API_KEY"],
|
|
24
|
+
mistral: ["MISTRAL_API_KEY"],
|
|
25
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
26
|
+
together: ["TOGETHER_API_KEY"],
|
|
27
|
+
perplexity: ["PERPLEXITY_API_KEY"],
|
|
28
|
+
xai: ["XAI_API_KEY"],
|
|
29
|
+
fireworks: ["FIREWORKS_API_KEY"],
|
|
30
|
+
cerebras: ["CEREBRAS_API_KEY"],
|
|
31
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
32
|
+
}
|
|
33
|
+
|
|
6
34
|
export function resolveApiKey(raw: string | undefined): string | undefined {
|
|
7
35
|
if (!raw) return undefined
|
|
8
36
|
const trimmed = raw.trim()
|
|
@@ -24,24 +52,70 @@ export function resolveApiKey(raw: string | undefined): string | undefined {
|
|
|
24
52
|
return trimmed
|
|
25
53
|
}
|
|
26
54
|
|
|
55
|
+
function resolveProviderApiKey(
|
|
56
|
+
opts: ResolvedOptions,
|
|
57
|
+
providerID: string | undefined,
|
|
58
|
+
providerConfig: any,
|
|
59
|
+
providerState: any,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
if (opts.apiKey) return resolveApiKey(opts.apiKey)
|
|
62
|
+
|
|
63
|
+
const configKey = resolveApiKey(providerConfig?.options?.apiKey)
|
|
64
|
+
if (configKey) return configKey
|
|
65
|
+
|
|
66
|
+
const stateOptionKey = resolveApiKey(providerState?.options?.apiKey)
|
|
67
|
+
if (stateOptionKey) return stateOptionKey
|
|
68
|
+
|
|
69
|
+
const stateKey = resolveApiKey(providerState?.key)
|
|
70
|
+
if (stateKey) return stateKey
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(providerState?.env)) {
|
|
73
|
+
for (const envName of providerState.env) {
|
|
74
|
+
if (typeof envName === "string" && process.env[envName]) {
|
|
75
|
+
return process.env[envName]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (providerID && WELL_KNOWN_ENV_KEYS[providerID]) {
|
|
81
|
+
for (const envName of WELL_KNOWN_ENV_KEYS[providerID]) {
|
|
82
|
+
if (process.env[envName]) {
|
|
83
|
+
return process.env[envName]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (process.env.LITELLM_API_KEY) return process.env.LITELLM_API_KEY
|
|
89
|
+
if (!providerID || providerID === "openai" || providerID === "custom") {
|
|
90
|
+
if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
27
96
|
export async function fetchStatelessSuggestion(
|
|
28
97
|
transcript: string,
|
|
29
98
|
opts: ResolvedOptions,
|
|
30
99
|
modelRef: { providerID: string; modelID: string } | undefined,
|
|
31
100
|
config: any,
|
|
101
|
+
providers?: readonly any[],
|
|
32
102
|
): Promise<string | undefined> {
|
|
33
|
-
const
|
|
103
|
+
const providerID = modelRef?.providerID
|
|
104
|
+
const providerConfig = providerID ? config?.provider?.[providerID] : undefined
|
|
105
|
+
const providerState = providerID && Array.isArray(providers)
|
|
106
|
+
? providers.find((p) => p?.id === providerID)
|
|
107
|
+
: undefined
|
|
108
|
+
|
|
109
|
+
const apiKey = resolveProviderApiKey(opts, providerID, providerConfig, providerState)
|
|
110
|
+
const isAnthropic = providerID === "anthropic"
|
|
111
|
+
|
|
34
112
|
const baseURL =
|
|
35
113
|
opts.endpoint ||
|
|
36
|
-
|
|
114
|
+
providerConfig?.options?.baseURL ||
|
|
115
|
+
providerState?.options?.baseURL ||
|
|
116
|
+
(!isAnthropic && providerID ? WELL_KNOWN_BASE_URLS[providerID] : undefined) ||
|
|
37
117
|
process.env.LITELLM_BASE_URL ||
|
|
38
118
|
process.env.OPENAI_BASE_URL
|
|
39
|
-
const rawKey =
|
|
40
|
-
opts.apiKey ||
|
|
41
|
-
provider?.options?.apiKey ||
|
|
42
|
-
process.env.LITELLM_API_KEY ||
|
|
43
|
-
process.env.OPENAI_API_KEY
|
|
44
|
-
const apiKey = resolveApiKey(rawKey)
|
|
45
119
|
|
|
46
120
|
const controller = new AbortController()
|
|
47
121
|
const timeoutMs = 4000
|
|
@@ -54,7 +128,8 @@ export async function fetchStatelessSuggestion(
|
|
|
54
128
|
: `${baseURL.replace(/\/+$/, "")}/chat/completions`
|
|
55
129
|
|
|
56
130
|
const rawModel = modelRef?.modelID || "default"
|
|
57
|
-
const
|
|
131
|
+
const keepSlash = providerID === "openrouter" || providerID === "together"
|
|
132
|
+
const model = !keepSlash && rawModel.includes("/")
|
|
58
133
|
? rawModel.slice(rawModel.lastIndexOf("/") + 1)
|
|
59
134
|
: rawModel
|
|
60
135
|
|
|
@@ -82,9 +157,15 @@ export async function fetchStatelessSuggestion(
|
|
|
82
157
|
}
|
|
83
158
|
|
|
84
159
|
if (modelRef?.providerID === "anthropic") {
|
|
85
|
-
const antKey =
|
|
160
|
+
const antKey = apiKey || process.env.ANTHROPIC_API_KEY
|
|
86
161
|
if (!antKey) return undefined
|
|
87
|
-
const
|
|
162
|
+
const antBase = (
|
|
163
|
+
providerConfig?.options?.baseURL ||
|
|
164
|
+
providerState?.options?.baseURL ||
|
|
165
|
+
"https://api.anthropic.com"
|
|
166
|
+
).replace(/\/+$/, "")
|
|
167
|
+
const url = antBase.endsWith("/v1/messages") ? antBase : `${antBase}/v1/messages`
|
|
168
|
+
const res = await fetch(url, {
|
|
88
169
|
method: "POST",
|
|
89
170
|
headers: {
|
|
90
171
|
"Content-Type": "application/json",
|