@pellux/goodvibes-agent 0.1.59 → 0.1.61

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,199 +0,0 @@
1
- ---
2
- name: add-provider
3
- description: Adds custom LLM providers and models to GoodVibes Agent. Use when the user wants to add a provider, add a model, configure Ollama, Together AI, OpenRouter, Groq, LM Studio, Fireworks, vLLM, or any OpenAI-compatible endpoint.
4
- version: 1.0.0
5
- triggers:
6
- - /add-provider
7
- - add provider
8
- - add a provider
9
- - new provider
10
- - custom provider
11
- - add model
12
- - add a model
13
- author: goodvibes
14
- ---
15
-
16
- # Add Custom Provider
17
-
18
- Interactively collect provider and model details from the user, then write a JSON config to `~/.goodvibes/agent/providers/{name}.json`.
19
-
20
- ## Workflow
21
-
22
- ### Step 1: Check for Existing Provider
23
-
24
- Before collecting info, check if `~/.goodvibes/agent/providers/` already has JSON files. If the user names a provider that already exists, ask:
25
- - **Add models** to the existing provider, or
26
- - **Overwrite** it entirely
27
-
28
- If adding models, read the existing JSON, append new models, and write back.
29
-
30
- ### Step 2: Collect Provider Details
31
-
32
- Ask the user for each field. Apply smart defaults when the provider name matches a known service.
33
-
34
- #### Required Fields
35
-
36
- | Field | Description | Validation |
37
- |-------|-------------|------------|
38
- | `name` | Internal ID | Lowercase alphanumeric + hyphens only, 1-64 chars |
39
- | `displayName` | Human-readable name | Non-empty string |
40
- | `type` | API compatibility | `openai-compat` (recommended) or `anthropic-compat` (not yet supported — use `openai-compat`) |
41
- | `baseURL` | API endpoint | Must start with `http://` or `https://` |
42
-
43
- > **Note:** `anthropic-compat` is accepted in the JSON schema for forward compatibility but is not yet functional at runtime. The loader will skip configs with this type and emit a warning. Use `openai-compat` for now — most Anthropic-compatible proxies (e.g., via LiteLLM) expose an OpenAI-compatible endpoint.
44
-
45
- #### Optional Fields
46
-
47
- | Field | Description | Default |
48
- |-------|-------------|---------|
49
- | `apiKeyEnv` | Environment variable for API key | None |
50
- | `apiKey` | Explicit API key (not recommended) | None |
51
- | `defaultHeaders` | Custom HTTP headers sent with every API request (e.g., for proxy authentication or routing) | None |
52
-
53
- ### Smart Defaults
54
-
55
- When the user mentions a known provider, pre-fill these values and confirm:
56
-
57
- ```yaml
58
- ollama:
59
- displayName: Ollama
60
- type: openai-compat
61
- baseURL: http://localhost:11434/v1
62
- apiKeyEnv: null
63
-
64
- together:
65
- displayName: Together AI
66
- type: openai-compat
67
- baseURL: https://api.together.xyz/v1
68
- apiKeyEnv: TOGETHER_API_KEY
69
-
70
- openrouter:
71
- displayName: OpenRouter
72
- type: openai-compat
73
- baseURL: https://openrouter.ai/api/v1
74
- apiKeyEnv: OPENROUTER_API_KEY
75
-
76
- groq:
77
- displayName: Groq
78
- type: openai-compat
79
- baseURL: https://api.groq.com/openai/v1
80
- apiKeyEnv: GROQ_API_KEY
81
-
82
- lm-studio:
83
- displayName: LM Studio
84
- type: openai-compat
85
- baseURL: http://localhost:1234/v1
86
- apiKeyEnv: null
87
-
88
- fireworks:
89
- displayName: Fireworks AI
90
- type: openai-compat
91
- baseURL: https://api.fireworks.ai/inference/v1
92
- apiKeyEnv: FIREWORKS_API_KEY
93
-
94
- vllm:
95
- displayName: vLLM
96
- type: openai-compat
97
- baseURL: http://localhost:8000/v1
98
- apiKeyEnv: null
99
- ```
100
-
101
- If the name does not match a known provider, ask for all fields individually.
102
-
103
- ### Step 3: Collect Model Details
104
-
105
- Collect at least one model. For each model:
106
-
107
- | Field | Description | Validation |
108
- |-------|-------------|------------|
109
- | `id` | Model identifier sent to the API | Non-empty string |
110
- | `displayName` | Human-readable name | Non-empty string |
111
- | `description` | Short description | Optional, auto-generate if blank |
112
- | `contextWindow` | Max tokens in context | Positive integer |
113
- | `capabilities.toolCalling` | Supports function/tool calling | Boolean, default `true` |
114
- | `capabilities.codeEditing` | Good at code editing tasks | Boolean, default `true` |
115
- | `capabilities.reasoning` | Extended reasoning/chain-of-thought | Boolean, default `false` |
116
- | `capabilities.multimodal` | Supports image input | Boolean, default `false` |
117
- | `reasoningEffort` | Supported reasoning effort levels | Optional, e.g., `["low", "medium", "high"]` |
118
-
119
- After each model, ask: "Add another model?" Loop until done.
120
-
121
- ### Step 4: Preview and Confirm
122
-
123
- Show the complete JSON to the user and ask for confirmation before writing.
124
-
125
- Example output:
126
-
127
- ```json
128
- {
129
- "name": "ollama",
130
- "displayName": "Ollama",
131
- "type": "openai-compat",
132
- "baseURL": "http://localhost:11434/v1",
133
- "models": [
134
- {
135
- "id": "llama3.3-70b",
136
- "displayName": "Llama 3.3 70B",
137
- "description": "Meta's Llama 3.3 70B parameter model",
138
- "contextWindow": 131072,
139
- "capabilities": {
140
- "toolCalling": true,
141
- "codeEditing": true,
142
- "reasoning": true,
143
- "multimodal": false
144
- },
145
- "reasoningEffort": ["low", "medium", "high"]
146
- }
147
- ]
148
- }
149
- ```
150
-
151
- ### Step 5: Write the File
152
-
153
- Write to `~/.goodvibes/agent/providers/{name}.json`. Create the directory if it does not exist.
154
-
155
- Use `precision_write` with `mode: "fail_if_exists"` for new providers. Use `mode: "overwrite"` when the user chose to overwrite an existing provider or when merging models into an existing file.
156
-
157
- If the `apiKeyEnv` field is set, include it in the JSON. If the user provided an explicit `apiKey`, include it but warn that storing keys in plain text is not recommended -- suggest using an environment variable instead.
158
-
159
- If `defaultHeaders` were provided, include them.
160
-
161
- ### Step 6: Confirm
162
-
163
- Tell the user:
164
- - The file was written to `~/.goodvibes/agent/providers/{name}.json`
165
- - The provider should be available to the Agent runtime after provider reload or next Agent startup
166
- - If an API key is needed, remind them to set the environment variable
167
-
168
- ## Validation Rules
169
-
170
- Before writing, verify:
171
- 1. `name` matches `/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/` and is 1-64 characters (no trailing hyphens)
172
- 2. `baseURL` starts with `http://` or `https://`
173
- 3. `contextWindow` is a positive integer for every model
174
- 4. At least one model is defined
175
- 5. Every model has a non-empty `id` and `displayName`
176
-
177
- If validation fails, tell the user which field is invalid and ask for correction.
178
-
179
- ## JSON Schema Reference
180
-
181
- The provider JSON maps to the codebase types:
182
-
183
- - Provider registration uses `OpenAICompatProvider` for `openai-compat` type. The `anthropic-compat` type is parsed and accepted in the JSON but is **not yet supported at runtime** — the loader skips it with a warning. Recommend `openai-compat` for all custom providers.
184
- - `OpenAICompatOptions`: `{ name, baseURL, apiKey, defaultModel, models }`
185
- - Model entries map to `ModelDefinition`: `{ id, provider, displayName, description, contextWindow, capabilities, reasoningEffort?, selectable }`
186
- - `reasoningEffort?: string[]` — optional array of supported effort levels, e.g., `["low", "medium", "high"]`
187
- - The `selectable` field defaults to `true` for custom models
188
- - The `provider` field in `ModelDefinition` is auto-set to the provider `name`
189
-
190
- ## Conversational Style
191
-
192
- Be natural and helpful. Guide users step-by-step but do not be overly verbose. If the user provides multiple details at once (e.g., "add ollama with llama3.3-70b"), extract what you can and only ask for missing fields.
193
-
194
- ## Edge Cases
195
-
196
- - **Unknown context window**: Suggest 4096 as a safe default, note the user can update later
197
- - **No API key needed**: Omit `apiKeyEnv` and `apiKey` from the JSON entirely
198
- - **User provides a full JSON blob**: Validate it against the schema and write directly
199
- - **Multiple providers in one session**: After completing one, ask if they want to add another
@@ -1,223 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import type { CommandContext, CommandRegistry } from '../command-registry.ts';
4
- import { CodeIntelligence } from '@pellux/goodvibes-sdk/platform/intelligence';
5
- import type { DocumentSymbol } from '@pellux/goodvibes-sdk/platform/intelligence';
6
- import type { SymbolInfo } from '@pellux/goodvibes-sdk/platform/intelligence';
7
- import { openCommandPanel, requireReadModels, requireShellPaths } from './runtime-services.ts';
8
-
9
- function resolveTargetPath(pathArg: string, ctx: CommandContext): string {
10
- return requireShellPaths(ctx).resolveWorkspacePath(pathArg);
11
- }
12
-
13
- function parsePosition(lineArg: string | undefined, columnArg: string | undefined): { line: number; column: number } | null {
14
- const line = Number.parseInt(lineArg ?? '', 10);
15
- const column = Number.parseInt(columnArg ?? '', 10);
16
- if (!Number.isFinite(line) || line < 1 || !Number.isFinite(column) || column < 1) return null;
17
- return { line: line - 1, column: column - 1 };
18
- }
19
-
20
- function formatSymbolKind(kind: number | string | undefined): string {
21
- if (typeof kind === 'number') return `kind=${kind}`;
22
- if (typeof kind === 'string' && kind.trim().length > 0) return kind;
23
- return 'symbol';
24
- }
25
-
26
- function formatDocumentSymbol(symbol: DocumentSymbol): string {
27
- const line = (symbol.selectionRange?.start.line ?? symbol.range.start.line) + 1;
28
- const column = (symbol.selectionRange?.start.character ?? symbol.range.start.character) + 1;
29
- return ` ${symbol.name} ${formatSymbolKind(symbol.kind)} ${line}:${column}`;
30
- }
31
-
32
- function formatTreeSitterSymbol(symbol: SymbolInfo): string {
33
- return ` ${symbol.name} ${formatSymbolKind(symbol.kind)} ${symbol.line + 1}:${symbol.column + 1}`;
34
- }
35
-
36
- function ensureExistingFile(pathArg: string | undefined, ctx: CommandContext): string | null {
37
- if (!pathArg) {
38
- ctx.print('Intelligence Review\n Missing file path.');
39
- return null;
40
- }
41
- const targetPath = resolveTargetPath(pathArg, ctx);
42
- if (!existsSync(targetPath)) {
43
- ctx.print(`Intelligence Review\n File not found: ${targetPath}`);
44
- return null;
45
- }
46
- return targetPath;
47
- }
48
-
49
- export function registerIntelligenceRuntimeCommands(registry: CommandRegistry): void {
50
- registry.register({
51
- name: 'intelligence',
52
- aliases: ['intel'],
53
- description: 'Review workspace intelligence readiness, diagnostics posture, and symbol search availability',
54
- usage: '[review|panel|diagnostics [file]|symbols <file>|outline <file>|definition <file> <line> <column>|references <file> <line> <column>|hover <file> <line> <column>|repair]',
55
- async handler(args, ctx) {
56
- const sub = (args[0] ?? 'review').toLowerCase();
57
- if (sub === 'panel' || sub === 'open') {
58
- openCommandPanel(ctx, 'intelligence');
59
- return;
60
- }
61
-
62
- const intelligence = new CodeIntelligence();
63
- const state = requireReadModels(ctx).intelligence.getSnapshot();
64
-
65
- if (sub === 'symbols' || sub === 'outline') {
66
- const targetPath = ensureExistingFile(args[1], ctx);
67
- if (!targetPath) return;
68
- const content = readFileSync(targetPath, 'utf-8');
69
- if (sub === 'symbols') {
70
- const symbols = await intelligence.getDocumentSymbols(targetPath, content);
71
- const entries = symbols.slice(0, 12).map((symbol) => ('selectionRange' in symbol ? formatDocumentSymbol(symbol) : formatTreeSitterSymbol(symbol)));
72
- ctx.print([
73
- `Intelligence Symbols: ${targetPath}`,
74
- ` source: ${state.symbolSearchStatus === 'ready' ? 'LSP/tree-sitter' : 'best-effort tree-sitter/LSP fallback'}`,
75
- ` status: ${state.symbolSearchStatus}`,
76
- ` results: ${symbols.length}`,
77
- ...(entries.length > 0 ? entries : [' No symbols available for this file.']),
78
- ' next: /health intelligence',
79
- ].join('\n'));
80
- return;
81
- }
82
-
83
- const outline = await intelligence.getOutline(targetPath, content);
84
- ctx.print([
85
- `Intelligence Outline: ${targetPath}`,
86
- ` source: tree-sitter outline extraction`,
87
- ` language ready: ${intelligence.hasTreeSitter(targetPath) ? 'yes' : 'no'}`,
88
- ` results: ${outline.length}`,
89
- ...(outline.slice(0, 12).map((entry) => ` ${entry.signature || entry.name} line ${entry.line}`)),
90
- ...(outline.length === 0 ? [' No outline entries available for this file.'] : []),
91
- ' next: /intelligence symbols ' + targetPath,
92
- ].join('\n'));
93
- return;
94
- }
95
-
96
- if (sub === 'definition' || sub === 'references' || sub === 'hover') {
97
- const targetPath = ensureExistingFile(args[1], ctx);
98
- if (!targetPath) return;
99
- const position = parsePosition(args[2], args[3]);
100
- if (!position) {
101
- ctx.print(`Intelligence ${sub[0]!.toUpperCase()}${sub.slice(1)}\n Usage: /intelligence ${sub} <file> <line> <column>`);
102
- return;
103
- }
104
-
105
- if (sub === 'definition') {
106
- const definition = await intelligence.getDefinition(targetPath, position.line, position.column);
107
- ctx.print([
108
- `Intelligence Definition: ${targetPath}:${position.line + 1}:${position.column + 1}`,
109
- ` status: ${state.hoverStatus === 'ready' || state.symbolSearchStatus === 'ready' ? 'available' : 'best-effort'}`,
110
- ...(definition
111
- ? [
112
- ` target: ${definition.uri}`,
113
- ` line: ${definition.range.start.line + 1}`,
114
- ` column: ${definition.range.start.character + 1}`,
115
- ' next: open the target file or use /intelligence references on the same symbol',
116
- ]
117
- : [' No definition was returned for that position.', ' next: /health intelligence']),
118
- ].join('\n'));
119
- return;
120
- }
121
-
122
- if (sub === 'references') {
123
- const references = await intelligence.getReferences(targetPath, position.line, position.column);
124
- ctx.print([
125
- `Intelligence References: ${targetPath}:${position.line + 1}:${position.column + 1}`,
126
- ` status: ${state.symbolSearchStatus}`,
127
- ` results: ${references.length}`,
128
- ...(references.slice(0, 12).map((reference) => ` ${reference.uri} ${reference.range.start.line + 1}:${reference.range.start.character + 1}`)),
129
- ...(references.length === 0 ? [' No references were returned for that position.'] : []),
130
- ' next: /intelligence definition ' + `${targetPath} ${position.line + 1} ${position.column + 1}`,
131
- ].join('\n'));
132
- return;
133
- }
134
-
135
- const hover = await intelligence.getHover(targetPath, position.line, position.column);
136
- const hoverLines = typeof hover?.contents === 'string'
137
- ? hover.contents.split('\n')
138
- : Array.isArray(hover?.contents)
139
- ? hover.contents.flatMap((entry) => typeof entry === 'string' ? entry : entry.value.split('\n'))
140
- : hover?.contents && 'value' in hover.contents
141
- ? hover.contents.value.split('\n')
142
- : [];
143
- ctx.print([
144
- `Intelligence Hover: ${targetPath}:${position.line + 1}:${position.column + 1}`,
145
- ` status: ${state.hoverStatus}`,
146
- ...(hoverLines.length > 0 ? hoverLines.slice(0, 8).map((line) => ` ${line}`) : [' No hover information was returned for that position.']),
147
- ' next: /health intelligence',
148
- ].join('\n'));
149
- return;
150
- }
151
-
152
- if (sub === 'diagnostics') {
153
- const file = args[1];
154
- const entries = [...state.diagnostics.entries()]
155
- .map(([filePath, diagnostics]) => ({
156
- filePath,
157
- diagnostics,
158
- errors: diagnostics.filter((entry) => entry.severity === 'error').length,
159
- warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
160
- }))
161
- .sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath));
162
- const selected = file
163
- ? entries.find((entry) => entry.filePath === file)
164
- : entries[0];
165
- if (!selected) {
166
- ctx.print('Intelligence Diagnostics\n No diagnostics are currently tracked.');
167
- return;
168
- }
169
- ctx.print([
170
- `Intelligence Diagnostics: ${selected.filePath}`,
171
- ` errors: ${selected.errors}`,
172
- ` warnings: ${selected.warnings}`,
173
- ...selected.diagnostics.slice(0, 8).map((diagnostic) => (
174
- ` [${diagnostic.severity}] ${diagnostic.line + 1}:${diagnostic.column + 1} ${diagnostic.message}`
175
- )),
176
- ...(entries.length > 1 ? [` next: /intelligence diagnostics ${entries[1]!.filePath}`] : []),
177
- ].join('\n'));
178
- return;
179
- }
180
-
181
- if (sub === 'repair') {
182
- const lines = [
183
- 'Intelligence Repair',
184
- ' verify: /health intelligence',
185
- ...(state.diagnosticsStatus !== 'ready' ? [' /setup review', ' /health intelligence'] : []),
186
- ...(state.symbolSearchStatus !== 'ready' ? [' /symbols', ' /intelligence symbols <file>', ' /health intelligence'] : []),
187
- ...(state.completionsStatus !== 'ready' || state.hoverStatus !== 'ready'
188
- ? [' /intelligence review', ' /intelligence hover <file> <line> <column>', ' /setup onboarding']
189
- : []),
190
- ];
191
- ctx.print(lines.length > 1 ? lines.join('\n') : 'Intelligence Repair\n No active repair actions suggested.');
192
- return;
193
- }
194
-
195
- const issues: string[] = [];
196
- if (state.diagnosticsStatus !== 'ready') issues.push(`diagnostics=${state.diagnosticsStatus}`);
197
- if (state.symbolSearchStatus !== 'ready') issues.push(`symbols=${state.symbolSearchStatus}`);
198
- if (state.completionsStatus !== 'ready') issues.push(`completions=${state.completionsStatus}`);
199
- if (state.hoverStatus !== 'ready') issues.push(`hover=${state.hoverStatus}`);
200
-
201
- ctx.print([
202
- 'Intelligence Review',
203
- ` diagnostics: ${state.diagnosticsStatus}`,
204
- ` symbols: ${state.symbolSearchStatus}`,
205
- ` completions: ${state.completionsStatus}`,
206
- ` hover: ${state.hoverStatus}`,
207
- ` errors: ${state.errorCount}`,
208
- ` warnings: ${state.warningCount}`,
209
- ` requests: ${state.totalRequests}`,
210
- ` avg latency: ${Math.round(state.avgLatencyMs)}ms`,
211
- ...(issues.length > 0 ? [` issues: ${issues.join(', ')}`] : [' issues: none']),
212
- ` diagnostic files: ${state.diagnostics.size}`,
213
- ...(state.diagnostics.size > 0 ? [' next: /intelligence diagnostics'] : []),
214
- ' next: /intelligence symbols <file>',
215
- ' next: /intelligence outline <file>',
216
- ' next: /intelligence references <file> <line> <column>',
217
- ' next: /intelligence definition <file> <line> <column>',
218
- ' next: /intelligence hover <file> <line> <column>',
219
- ' next: /intelligence repair',
220
- ].join('\n'));
221
- },
222
- });
223
- }