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 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.3.0...HEAD
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.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": "5.8.2"
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 provider = modelRef?.providerID ? config?.provider?.[modelRef.providerID] : undefined
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
- provider?.options?.baseURL ||
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 model = rawModel.includes("/")
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 = resolveApiKey(provider?.options?.apiKey) || process.env.ANTHROPIC_API_KEY
160
+ const antKey = apiKey || process.env.ANTHROPIC_API_KEY
86
161
  if (!antKey) return undefined
87
- const res = await fetch("https://api.anthropic.com/v1/messages", {
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",
package/src/tui.tsx CHANGED
@@ -631,6 +631,7 @@ const tui: TuiPlugin = async (api, rawOptions) => {
631
631
  opts,
632
632
  model,
633
633
  api.state.config,
634
+ api.state.provider,
634
635
  )
635
636
  if (stateless) {
636
637
  const suggestion = normalize(stateless, opts.maxChars)