gsd-pi 2.11.0 → 2.13.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/dist/cli.js +18 -1
- package/dist/onboarding.js +3 -0
- package/dist/resource-loader.d.ts +2 -0
- package/dist/resource-loader.js +36 -1
- package/dist/resources/extensions/bg-shell/index.ts +51 -7
- package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/dist/resources/extensions/gsd/auto.ts +381 -13
- package/dist/resources/extensions/gsd/commands.ts +9 -3
- package/dist/resources/extensions/gsd/doctor.ts +254 -3
- package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/dist/resources/extensions/gsd/git-service.ts +11 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/dist/resources/extensions/gsd/preferences.ts +209 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +32 -29
- package/dist/resources/extensions/gsd/templates/context.md +1 -1
- package/dist/resources/extensions/gsd/templates/state.md +3 -3
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/dist/resources/extensions/gsd/types.ts +109 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
- package/dist/resources/extensions/search-the-web/provider.ts +19 -2
- package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
- package/dist/wizard.js +1 -0
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +169 -55
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +13 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +16 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +91 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +273 -63
- package/packages/pi-agent-core/src/agent.ts +24 -0
- package/packages/pi-agent-core/src/types.ts +98 -0
- package/packages/pi-ai/dist/env-api-keys.js +1 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +314 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +236 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/env-api-keys.ts +1 -0
- package/packages/pi-ai/src/models.generated.ts +236 -0
- package/packages/pi-ai/src/types.ts +2 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +2 -1
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +69 -8
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +2 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
- package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
- package/packages/pi-tui/dist/components/editor.d.ts +11 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +64 -6
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +71 -6
- package/src/resources/extensions/bg-shell/index.ts +51 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/src/resources/extensions/gsd/auto.ts +381 -13
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/doctor.ts +254 -3
- package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/src/resources/extensions/gsd/git-service.ts +11 -0
- package/src/resources/extensions/gsd/guided-flow.ts +81 -9
- package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/src/resources/extensions/gsd/preferences.ts +209 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/queue.md +3 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +32 -29
- package/src/resources/extensions/gsd/templates/context.md +1 -1
- package/src/resources/extensions/gsd/templates/state.md +3 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/src/resources/extensions/gsd/types.ts +109 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/src/resources/extensions/search-the-web/native-search.ts +15 -10
- package/src/resources/extensions/search-the-web/provider.ts +19 -2
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
|
@@ -185,3 +185,112 @@ export interface GSDState {
|
|
|
185
185
|
tasks?: { done: number; total: number };
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
// ─── Post-Unit Hook Types ─────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export interface PostUnitHookConfig {
|
|
192
|
+
/** Unique hook identifier — used in idempotency keys and logging. */
|
|
193
|
+
name: string;
|
|
194
|
+
/** Unit types that trigger this hook (e.g., ["execute-task"]). */
|
|
195
|
+
after: string[];
|
|
196
|
+
/** Prompt sent to the LLM session. Supports {milestoneId}, {sliceId}, {taskId} substitutions. */
|
|
197
|
+
prompt: string;
|
|
198
|
+
/** Max times this hook can fire for the same trigger unit. Default 1, max 10. */
|
|
199
|
+
max_cycles?: number;
|
|
200
|
+
/** Model override for hook sessions. */
|
|
201
|
+
model?: string;
|
|
202
|
+
/** Expected output file name (relative to task/slice dir). Used for idempotency — skip if exists. */
|
|
203
|
+
artifact?: string;
|
|
204
|
+
/** If this file is produced instead of artifact, re-run the trigger unit then re-run hooks. */
|
|
205
|
+
retry_on?: string;
|
|
206
|
+
/** Agent definition file to use. */
|
|
207
|
+
agent?: string;
|
|
208
|
+
/** Set false to disable without removing config. Default true. */
|
|
209
|
+
enabled?: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface HookExecutionState {
|
|
213
|
+
/** Hook name. */
|
|
214
|
+
hookName: string;
|
|
215
|
+
/** The unit type that triggered this hook. */
|
|
216
|
+
triggerUnitType: string;
|
|
217
|
+
/** The unit ID that triggered this hook. */
|
|
218
|
+
triggerUnitId: string;
|
|
219
|
+
/** Current cycle (1-based). */
|
|
220
|
+
cycle: number;
|
|
221
|
+
/** Whether the hook completed with a retry signal (retry_on artifact found). */
|
|
222
|
+
pendingRetry: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface HookDispatchResult {
|
|
226
|
+
/** Hook name for display. */
|
|
227
|
+
hookName: string;
|
|
228
|
+
/** The prompt to send. */
|
|
229
|
+
prompt: string;
|
|
230
|
+
/** Model override, if configured. */
|
|
231
|
+
model?: string;
|
|
232
|
+
/** Synthetic unit type, e.g. "hook/code-review". */
|
|
233
|
+
unitType: string;
|
|
234
|
+
/** The trigger unit's ID, reused for the hook. */
|
|
235
|
+
unitId: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ─── Pre-Dispatch Hook Types ──────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
export interface PreDispatchHookConfig {
|
|
241
|
+
/** Unique hook identifier. */
|
|
242
|
+
name: string;
|
|
243
|
+
/** Unit types this hook intercepts before dispatch (e.g., ["execute-task"]). */
|
|
244
|
+
before: string[];
|
|
245
|
+
/** Action to take: "modify" mutates the prompt, "skip" skips the unit, "replace" swaps it. */
|
|
246
|
+
action: 'modify' | 'skip' | 'replace';
|
|
247
|
+
/** For "modify": text prepended to the unit prompt. Supports {milestoneId}, {sliceId}, {taskId}. */
|
|
248
|
+
prepend?: string;
|
|
249
|
+
/** For "modify": text appended to the unit prompt. Supports {milestoneId}, {sliceId}, {taskId}. */
|
|
250
|
+
append?: string;
|
|
251
|
+
/** For "replace": the replacement prompt. Supports {milestoneId}, {sliceId}, {taskId}. */
|
|
252
|
+
prompt?: string;
|
|
253
|
+
/** For "replace": override the unit type label. */
|
|
254
|
+
unit_type?: string;
|
|
255
|
+
/** For "skip": optional condition file — only skip if this file exists (relative to unit dir). */
|
|
256
|
+
skip_if?: string;
|
|
257
|
+
/** Model override when this hook fires. */
|
|
258
|
+
model?: string;
|
|
259
|
+
/** Set false to disable without removing config. Default true. */
|
|
260
|
+
enabled?: boolean;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface PreDispatchResult {
|
|
264
|
+
/** What happened: the unit proceeds with modifications, was skipped, or was replaced. */
|
|
265
|
+
action: 'proceed' | 'skip' | 'replace';
|
|
266
|
+
/** Modified/replacement prompt (for "proceed" and "replace"). */
|
|
267
|
+
prompt?: string;
|
|
268
|
+
/** Override unit type (for "replace"). */
|
|
269
|
+
unitType?: string;
|
|
270
|
+
/** Model override. */
|
|
271
|
+
model?: string;
|
|
272
|
+
/** Names of hooks that fired, for logging. */
|
|
273
|
+
firedHooks: string[];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Hook State Persistence Types ─────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
export interface PersistedHookState {
|
|
279
|
+
/** Cycle counts keyed as "hookName/triggerUnitType/triggerUnitId". */
|
|
280
|
+
cycleCounts: Record<string, number>;
|
|
281
|
+
/** Timestamp of last state save. */
|
|
282
|
+
savedAt: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface HookStatusEntry {
|
|
286
|
+
/** Hook name. */
|
|
287
|
+
name: string;
|
|
288
|
+
/** Hook type: "post" or "pre". */
|
|
289
|
+
type: 'post' | 'pre';
|
|
290
|
+
/** Whether hook is enabled. */
|
|
291
|
+
enabled: boolean;
|
|
292
|
+
/** What unit types it targets. */
|
|
293
|
+
targets: string[];
|
|
294
|
+
/** Current cycle counts for active triggers. */
|
|
295
|
+
activeCycles: Record<string, number>;
|
|
296
|
+
}
|
|
@@ -120,15 +120,17 @@ export function worktreeBranchName(name: string): string {
|
|
|
120
120
|
/**
|
|
121
121
|
* Create a new git worktree under .gsd/worktrees/<name>/ with branch worktree/<name>.
|
|
122
122
|
* The branch is created from the current HEAD of the main branch.
|
|
123
|
+
*
|
|
124
|
+
* @param opts.branch — override the default `worktree/<name>` branch name
|
|
123
125
|
*/
|
|
124
|
-
export function createWorktree(basePath: string, name: string): WorktreeInfo {
|
|
126
|
+
export function createWorktree(basePath: string, name: string, opts: { branch?: string } = {}): WorktreeInfo {
|
|
125
127
|
// Validate name: alphanumeric, hyphens, underscores only
|
|
126
128
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
127
129
|
throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
const wtPath = worktreePath(basePath, name);
|
|
131
|
-
const branch = worktreeBranchName(name);
|
|
133
|
+
const branch = opts.branch ?? worktreeBranchName(name);
|
|
132
134
|
|
|
133
135
|
if (existsSync(wtPath)) {
|
|
134
136
|
throw new Error(`Worktree "${name}" already exists at ${wtPath}`);
|
|
@@ -260,11 +262,11 @@ export function listWorktrees(basePath: string): WorktreeInfo[] {
|
|
|
260
262
|
export function removeWorktree(
|
|
261
263
|
basePath: string,
|
|
262
264
|
name: string,
|
|
263
|
-
opts: { deleteBranch?: boolean; force?: boolean } = {},
|
|
265
|
+
opts: { deleteBranch?: boolean; force?: boolean; branch?: string } = {},
|
|
264
266
|
): void {
|
|
265
267
|
const wtPath = worktreePath(basePath, name);
|
|
266
268
|
const resolvedWtPath = existsSync(wtPath) ? realpathSync(wtPath) : wtPath;
|
|
267
|
-
const branch = worktreeBranchName(name);
|
|
269
|
+
const branch = opts.branch ?? worktreeBranchName(name);
|
|
268
270
|
const { deleteBranch = true, force = false } = opts;
|
|
269
271
|
|
|
270
272
|
// If we're inside the worktree, move out first — git can't remove an in-use directory
|
|
@@ -13,16 +13,18 @@ import type { AutocompleteItem } from '@gsd/pi-tui'
|
|
|
13
13
|
import {
|
|
14
14
|
getTavilyApiKey,
|
|
15
15
|
getBraveApiKey,
|
|
16
|
+
getOllamaApiKey,
|
|
16
17
|
getSearchProviderPreference,
|
|
17
18
|
setSearchProviderPreference,
|
|
18
19
|
resolveSearchProvider,
|
|
19
20
|
type SearchProviderPreference,
|
|
20
21
|
} from './provider.ts'
|
|
21
22
|
|
|
22
|
-
const VALID_PREFERENCES: SearchProviderPreference[] = ['tavily', 'brave', 'auto']
|
|
23
|
+
const VALID_PREFERENCES: SearchProviderPreference[] = ['tavily', 'brave', 'ollama', 'auto']
|
|
23
24
|
|
|
24
|
-
function keyStatus(provider: 'tavily' | 'brave'): string {
|
|
25
|
+
function keyStatus(provider: 'tavily' | 'brave' | 'ollama'): string {
|
|
25
26
|
if (provider === 'tavily') return getTavilyApiKey() ? '✓' : '✗'
|
|
27
|
+
if (provider === 'ollama') return getOllamaApiKey() ? '✓' : '✗'
|
|
26
28
|
return getBraveApiKey() ? '✓' : '✗'
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -30,6 +32,7 @@ function buildSelectOptions(): string[] {
|
|
|
30
32
|
return [
|
|
31
33
|
`tavily (key: ${keyStatus('tavily')})`,
|
|
32
34
|
`brave (key: ${keyStatus('brave')})`,
|
|
35
|
+
`ollama (key: ${keyStatus('ollama')})`,
|
|
33
36
|
`auto`,
|
|
34
37
|
]
|
|
35
38
|
}
|
|
@@ -37,12 +40,13 @@ function buildSelectOptions(): string[] {
|
|
|
37
40
|
function parseSelectChoice(choice: string): SearchProviderPreference {
|
|
38
41
|
if (choice.startsWith('tavily')) return 'tavily'
|
|
39
42
|
if (choice.startsWith('brave')) return 'brave'
|
|
43
|
+
if (choice.startsWith('ollama')) return 'ollama'
|
|
40
44
|
return 'auto'
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
export function registerSearchProviderCommand(pi: ExtensionAPI): void {
|
|
44
48
|
pi.registerCommand('search-provider', {
|
|
45
|
-
description: 'Switch search provider (tavily, brave, auto)',
|
|
49
|
+
description: 'Switch search provider (tavily, brave, ollama, auto)',
|
|
46
50
|
|
|
47
51
|
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
48
52
|
const trimmed = prefix.trim().toLowerCase()
|
|
@@ -51,7 +55,7 @@ export function registerSearchProviderCommand(pi: ExtensionAPI): void {
|
|
|
51
55
|
.map((p) => {
|
|
52
56
|
let description: string
|
|
53
57
|
if (p === 'auto') {
|
|
54
|
-
description = `Auto-select (tavily: ${keyStatus('tavily')}, brave: ${keyStatus('brave')})`
|
|
58
|
+
description = `Auto-select (tavily: ${keyStatus('tavily')}, brave: ${keyStatus('brave')}, ollama: ${keyStatus('ollama')})`
|
|
55
59
|
} else {
|
|
56
60
|
description = `key: ${keyStatus(p)}`
|
|
57
61
|
}
|
|
@@ -105,16 +105,21 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
|
|
|
105
105
|
const payload = event.payload as Record<string, unknown>;
|
|
106
106
|
if (!payload) return;
|
|
107
107
|
|
|
108
|
-
// Detect Anthropic provider.
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
// Detect Anthropic provider. Use the model object from the event (most
|
|
109
|
+
// reliable — comes directly from the resolved Model), then fall back to
|
|
110
|
+
// the model_select flag, then to the model name heuristic (last resort).
|
|
111
|
+
// The model name heuristic is needed for session restores where
|
|
112
|
+
// modelsAreEqual suppresses model_select AND the SDK doesn't pass model.
|
|
113
|
+
const eventModel = event.model as { provider: string } | undefined;
|
|
114
|
+
let isAnthropic: boolean;
|
|
115
|
+
if (eventModel?.provider) {
|
|
116
|
+
isAnthropic = eventModel.provider === "anthropic";
|
|
117
|
+
} else if (modelSelectFired) {
|
|
118
|
+
isAnthropic = isAnthropicProvider;
|
|
119
|
+
} else {
|
|
120
|
+
const modelName = typeof payload.model === "string" ? payload.model : "";
|
|
121
|
+
isAnthropic = modelName.startsWith("claude-");
|
|
122
|
+
}
|
|
118
123
|
if (!isAnthropic) return;
|
|
119
124
|
|
|
120
125
|
// Strip thinking blocks from history to avoid signature validation errors
|
|
@@ -18,10 +18,10 @@ import { join } from 'path'
|
|
|
18
18
|
// where the relative import '../../../app-paths.ts' doesn't resolve.
|
|
19
19
|
const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json')
|
|
20
20
|
|
|
21
|
-
export type SearchProvider = 'tavily' | 'brave'
|
|
21
|
+
export type SearchProvider = 'tavily' | 'brave' | 'ollama'
|
|
22
22
|
export type SearchProviderPreference = SearchProvider | 'auto'
|
|
23
23
|
|
|
24
|
-
const VALID_PREFERENCES = new Set<string>(['tavily', 'brave', 'auto'])
|
|
24
|
+
const VALID_PREFERENCES = new Set<string>(['tavily', 'brave', 'ollama', 'auto'])
|
|
25
25
|
const PREFERENCE_KEY = 'search_provider'
|
|
26
26
|
|
|
27
27
|
/** Returns the Tavily API key from the environment, or empty string if not set. */
|
|
@@ -34,6 +34,11 @@ export function getBraveApiKey(): string {
|
|
|
34
34
|
return process.env.BRAVE_API_KEY || ''
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Returns the Ollama API key from the environment, or empty string if not set. */
|
|
38
|
+
export function getOllamaApiKey(): string {
|
|
39
|
+
return process.env.OLLAMA_API_KEY || ''
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/**
|
|
38
43
|
* Read the user's search provider preference from auth.json.
|
|
39
44
|
* Returns 'auto' if no preference is stored or the stored value is invalid.
|
|
@@ -78,9 +83,11 @@ export function setSearchProviderPreference(pref: SearchProviderPreference, auth
|
|
|
78
83
|
export function resolveSearchProvider(overridePreference?: string): SearchProvider | null {
|
|
79
84
|
const tavilyKey = getTavilyApiKey()
|
|
80
85
|
const braveKey = getBraveApiKey()
|
|
86
|
+
const ollamaKey = getOllamaApiKey()
|
|
81
87
|
|
|
82
88
|
const hasTavily = tavilyKey.length > 0
|
|
83
89
|
const hasBrave = braveKey.length > 0
|
|
90
|
+
const hasOllama = ollamaKey.length > 0
|
|
84
91
|
|
|
85
92
|
// Determine effective preference
|
|
86
93
|
let pref: SearchProviderPreference
|
|
@@ -100,18 +107,28 @@ export function resolveSearchProvider(overridePreference?: string): SearchProvid
|
|
|
100
107
|
if (pref === 'auto') {
|
|
101
108
|
if (hasTavily) return 'tavily'
|
|
102
109
|
if (hasBrave) return 'brave'
|
|
110
|
+
if (hasOllama) return 'ollama'
|
|
103
111
|
return null
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
if (pref === 'tavily') {
|
|
107
115
|
if (hasTavily) return 'tavily'
|
|
108
116
|
if (hasBrave) return 'brave'
|
|
117
|
+
if (hasOllama) return 'ollama'
|
|
109
118
|
return null
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
if (pref === 'brave') {
|
|
113
122
|
if (hasBrave) return 'brave'
|
|
114
123
|
if (hasTavily) return 'tavily'
|
|
124
|
+
if (hasOllama) return 'ollama'
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pref === 'ollama') {
|
|
129
|
+
if (hasOllama) return 'ollama'
|
|
130
|
+
if (hasTavily) return 'tavily'
|
|
131
|
+
if (hasBrave) return 'brave'
|
|
115
132
|
return null
|
|
116
133
|
}
|
|
117
134
|
|
|
@@ -17,6 +17,7 @@ import { LRUTTLCache } from "./cache.js";
|
|
|
17
17
|
import { fetchSimple, HttpError } from "./http.js";
|
|
18
18
|
import { extractDomain } from "./url-utils.js";
|
|
19
19
|
import { formatPageContent, type FormatPageOptions } from "./format.js";
|
|
20
|
+
import { getOllamaApiKey } from "./provider.js";
|
|
20
21
|
|
|
21
22
|
// =============================================================================
|
|
22
23
|
// Cache
|
|
@@ -173,6 +174,43 @@ async function fetchDirectFallback(
|
|
|
173
174
|
return { content: text, title, contentType };
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
// =============================================================================
|
|
178
|
+
// Ollama Web Fetch
|
|
179
|
+
// =============================================================================
|
|
180
|
+
|
|
181
|
+
interface OllamaWebFetchResponse {
|
|
182
|
+
title?: string;
|
|
183
|
+
content?: string;
|
|
184
|
+
links?: string[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Fetch page content via Ollama web_fetch API.
|
|
189
|
+
* Returns content + metadata, or throws on failure.
|
|
190
|
+
*/
|
|
191
|
+
async function fetchViaOllama(
|
|
192
|
+
url: string,
|
|
193
|
+
signal?: AbortSignal,
|
|
194
|
+
): Promise<{ content: string; title?: string }> {
|
|
195
|
+
const response = await fetchSimple("https://ollama.com/api/web_fetch", {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"Authorization": `Bearer ${getOllamaApiKey()}`,
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({ url }),
|
|
202
|
+
signal,
|
|
203
|
+
timeoutMs: 20_000,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const data: OllamaWebFetchResponse = await response.json();
|
|
207
|
+
|
|
208
|
+
const content = (data.content || "").trim();
|
|
209
|
+
const title = data.title?.trim() || undefined;
|
|
210
|
+
|
|
211
|
+
return { content, title };
|
|
212
|
+
}
|
|
213
|
+
|
|
176
214
|
// =============================================================================
|
|
177
215
|
// Smart Truncation
|
|
178
216
|
// =============================================================================
|
|
@@ -252,6 +290,30 @@ async function fetchOnePage(
|
|
|
252
290
|
jinaError = err instanceof HttpError
|
|
253
291
|
? `Jina HTTP ${err.statusCode}`
|
|
254
292
|
: (err as Error).message ?? String(err);
|
|
293
|
+
|
|
294
|
+
// Try Ollama web_fetch as intermediate fallback if API key is available
|
|
295
|
+
const ollamaKey = getOllamaApiKey();
|
|
296
|
+
if (ollamaKey) {
|
|
297
|
+
try {
|
|
298
|
+
const ollamaResult = await fetchViaOllama(url, options.signal);
|
|
299
|
+
if (ollamaResult.content && ollamaResult.content.length >= 50) {
|
|
300
|
+
pageContent = ollamaResult.content;
|
|
301
|
+
pageTitle = ollamaResult.title;
|
|
302
|
+
source = "direct";
|
|
303
|
+
return {
|
|
304
|
+
content: pageContent,
|
|
305
|
+
title: pageTitle,
|
|
306
|
+
source,
|
|
307
|
+
jinaError,
|
|
308
|
+
contentType,
|
|
309
|
+
originalChars: pageContent.length,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// Ollama fetch failed too — fall through to direct
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
255
317
|
source = "direct";
|
|
256
318
|
|
|
257
319
|
const result = await fetchDirectFallback(url, options.signal);
|
|
@@ -27,7 +27,7 @@ import { normalizeQuery, extractDomain } from "./url-utils.js";
|
|
|
27
27
|
import { formatLLMContext, type LLMContextSnippet, type LLMContextSource } from "./format.js";
|
|
28
28
|
import type { TavilyResult, TavilySearchResponse } from "./tavily.js";
|
|
29
29
|
import { publishedDateToAge } from "./tavily.js";
|
|
30
|
-
import { getTavilyApiKey, resolveSearchProvider } from "./provider.js";
|
|
30
|
+
import { getTavilyApiKey, getOllamaApiKey, resolveSearchProvider } from "./provider.js";
|
|
31
31
|
|
|
32
32
|
// =============================================================================
|
|
33
33
|
// Types
|
|
@@ -79,7 +79,7 @@ interface LLMContextDetails {
|
|
|
79
79
|
errorKind?: string;
|
|
80
80
|
error?: string;
|
|
81
81
|
retryAfterMs?: number;
|
|
82
|
-
provider?: 'tavily' | 'brave';
|
|
82
|
+
provider?: 'tavily' | 'brave' | 'ollama';
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// =============================================================================
|
|
@@ -230,6 +230,57 @@ async function executeTavilyLLMContext(
|
|
|
230
230
|
return { cached, latencyMs: timed.latencyMs, rateLimit: timed.rateLimit };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Ollama LLM Context Execution
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
interface OllamaWebSearchResult {
|
|
238
|
+
title: string;
|
|
239
|
+
url: string;
|
|
240
|
+
content: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface OllamaWebSearchResponse {
|
|
244
|
+
results: OllamaWebSearchResult[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Execute a search_and_read query against the Ollama web_search API.
|
|
249
|
+
*
|
|
250
|
+
* Uses the same web_search endpoint as tool-search, then applies
|
|
251
|
+
* budgetContent() for client-side token budgeting (similar to Tavily path).
|
|
252
|
+
*/
|
|
253
|
+
async function executeOllamaLLMContext(
|
|
254
|
+
params: { query: string; maxTokens: number; count: number; threshold: string },
|
|
255
|
+
signal?: AbortSignal,
|
|
256
|
+
): Promise<{ cached: CachedLLMContext; latencyMs: number; rateLimit?: RateLimitInfo }> {
|
|
257
|
+
const scoreThreshold = THRESHOLD_TO_SCORE[params.threshold] ?? 0.5;
|
|
258
|
+
|
|
259
|
+
const timed = await fetchWithRetryTimed("https://ollama.com/api/web_search", {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: {
|
|
262
|
+
"Content-Type": "application/json",
|
|
263
|
+
"Authorization": `Bearer ${getOllamaApiKey()}`,
|
|
264
|
+
},
|
|
265
|
+
body: JSON.stringify({ query: params.query, max_results: params.count }),
|
|
266
|
+
signal,
|
|
267
|
+
}, 2);
|
|
268
|
+
|
|
269
|
+
const data: OllamaWebSearchResponse = await timed.response.json();
|
|
270
|
+
|
|
271
|
+
// Convert Ollama results to TavilyResult-compatible format for budgetContent
|
|
272
|
+
const tavilyLikeResults: TavilyResult[] = (data.results || []).map(r => ({
|
|
273
|
+
title: r.title || "(untitled)",
|
|
274
|
+
url: r.url,
|
|
275
|
+
content: r.content || "",
|
|
276
|
+
score: 1.0, // Ollama doesn't provide scores, assume all are relevant
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
const cached = budgetContent(tavilyLikeResults, params.maxTokens, scoreThreshold);
|
|
280
|
+
|
|
281
|
+
return { cached, latencyMs: timed.latencyMs, rateLimit: timed.rateLimit };
|
|
282
|
+
}
|
|
283
|
+
|
|
233
284
|
// =============================================================================
|
|
234
285
|
// Tool Registration
|
|
235
286
|
// =============================================================================
|
|
@@ -295,7 +346,7 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
|
|
|
295
346
|
const provider = resolveSearchProvider();
|
|
296
347
|
if (!provider) {
|
|
297
348
|
return {
|
|
298
|
-
content: [{ type: "text", text: "search_and_read unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY or
|
|
349
|
+
content: [{ type: "text", text: "search_and_read unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, BRAVE_API_KEY, or OLLAMA_API_KEY." }],
|
|
299
350
|
isError: true,
|
|
300
351
|
details: { errorKind: "auth_error", error: "No search API key set" } satisfies Partial<LLMContextDetails>,
|
|
301
352
|
};
|
|
@@ -358,6 +409,14 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
|
|
|
358
409
|
result = tavilyResult.cached;
|
|
359
410
|
latencyMs = tavilyResult.latencyMs;
|
|
360
411
|
rateLimit = tavilyResult.rateLimit;
|
|
412
|
+
} else if (provider === "ollama") {
|
|
413
|
+
const ollamaResult = await executeOllamaLLMContext(
|
|
414
|
+
{ query: params.query, maxTokens, count, threshold },
|
|
415
|
+
signal,
|
|
416
|
+
);
|
|
417
|
+
result = ollamaResult.cached;
|
|
418
|
+
latencyMs = ollamaResult.latencyMs;
|
|
419
|
+
rateLimit = ollamaResult.rateLimit;
|
|
361
420
|
} else {
|
|
362
421
|
// ================================================================
|
|
363
422
|
// BRAVE PATH (unchanged API logic)
|
|
@@ -20,7 +20,7 @@ import { LRUTTLCache } from "./cache.js";
|
|
|
20
20
|
import { fetchWithRetryTimed, fetchWithRetry, classifyError, type RateLimitInfo } from "./http.js";
|
|
21
21
|
import { normalizeQuery, toDedupeKey, detectFreshness } from "./url-utils.js";
|
|
22
22
|
import { formatSearchResults, type SearchResultFormatted, type FormatSearchOptions } from "./format.js";
|
|
23
|
-
import { getTavilyApiKey, resolveSearchProvider } from "./provider.js";
|
|
23
|
+
import { getTavilyApiKey, getOllamaApiKey, resolveSearchProvider } from "./provider.js";
|
|
24
24
|
import { normalizeTavilyResult, mapFreshnessToTavily, type TavilySearchResponse } from "./tavily.js";
|
|
25
25
|
|
|
26
26
|
// =============================================================================
|
|
@@ -93,7 +93,7 @@ interface SearchDetails {
|
|
|
93
93
|
errorKind?: string;
|
|
94
94
|
error?: string;
|
|
95
95
|
retryAfterMs?: number;
|
|
96
|
-
provider?: 'tavily' | 'brave';
|
|
96
|
+
provider?: 'tavily' | 'brave' | 'ollama';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// =============================================================================
|
|
@@ -245,6 +245,57 @@ async function executeTavilySearch(
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// =============================================================================
|
|
249
|
+
// Ollama API execution
|
|
250
|
+
// =============================================================================
|
|
251
|
+
|
|
252
|
+
interface OllamaWebSearchResult {
|
|
253
|
+
title: string;
|
|
254
|
+
url: string;
|
|
255
|
+
content: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface OllamaWebSearchResponse {
|
|
259
|
+
results: OllamaWebSearchResult[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Execute a search against the Ollama web_search API.
|
|
264
|
+
* Returns a CachedSearchResult with normalized, deduplicated results.
|
|
265
|
+
*/
|
|
266
|
+
async function executeOllamaSearch(
|
|
267
|
+
params: { query: string; count: number },
|
|
268
|
+
signal?: AbortSignal
|
|
269
|
+
): Promise<{ results: CachedSearchResult; latencyMs: number; rateLimit?: RateLimitInfo }> {
|
|
270
|
+
const timed = await fetchWithRetryTimed("https://ollama.com/api/web_search", {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: {
|
|
273
|
+
"Content-Type": "application/json",
|
|
274
|
+
"Authorization": `Bearer ${getOllamaApiKey()}`,
|
|
275
|
+
},
|
|
276
|
+
body: JSON.stringify({ query: params.query, max_results: params.count }),
|
|
277
|
+
signal,
|
|
278
|
+
}, 2);
|
|
279
|
+
|
|
280
|
+
const data: OllamaWebSearchResponse = await timed.response.json();
|
|
281
|
+
const normalized: SearchResultFormatted[] = (data.results || []).map(r => ({
|
|
282
|
+
title: r.title || "(untitled)",
|
|
283
|
+
url: r.url,
|
|
284
|
+
description: r.content || "",
|
|
285
|
+
}));
|
|
286
|
+
const deduplicated = deduplicateResults(normalized);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
results: {
|
|
290
|
+
results: deduplicated,
|
|
291
|
+
queryCorrected: false,
|
|
292
|
+
moreResultsAvailable: false,
|
|
293
|
+
},
|
|
294
|
+
latencyMs: timed.latencyMs,
|
|
295
|
+
rateLimit: timed.rateLimit,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
248
299
|
// =============================================================================
|
|
249
300
|
// Tool Registration
|
|
250
301
|
// =============================================================================
|
|
@@ -300,7 +351,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
300
351
|
const provider = resolveSearchProvider();
|
|
301
352
|
if (!provider) {
|
|
302
353
|
return {
|
|
303
|
-
content: [{ type: "text", text: "Web search unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY or
|
|
354
|
+
content: [{ type: "text", text: "Web search unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, BRAVE_API_KEY, or OLLAMA_API_KEY." }],
|
|
304
355
|
isError: true,
|
|
305
356
|
details: { errorKind: "auth_error", error: "No search API key set" } satisfies Partial<SearchDetails>,
|
|
306
357
|
};
|
|
@@ -405,6 +456,14 @@ export function registerSearchTool(pi: ExtensionAPI) {
|
|
|
405
456
|
searchResult = tavilyResult.results;
|
|
406
457
|
latencyMs = tavilyResult.latencyMs;
|
|
407
458
|
rateLimit = tavilyResult.rateLimit;
|
|
459
|
+
} else if (provider === "ollama") {
|
|
460
|
+
const ollamaResult = await executeOllamaSearch(
|
|
461
|
+
{ query: params.query, count: 10 },
|
|
462
|
+
signal
|
|
463
|
+
);
|
|
464
|
+
searchResult = ollamaResult.results;
|
|
465
|
+
latencyMs = ollamaResult.latencyMs;
|
|
466
|
+
rateLimit = ollamaResult.rateLimit;
|
|
408
467
|
} else {
|
|
409
468
|
// ================================================================
|
|
410
469
|
// BRAVE PATH (unchanged API logic)
|