@oh-my-pi/pi-coding-agent 14.4.1 → 14.4.4

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +7 -7
  3. package/src/cli.ts +0 -1
  4. package/src/config/prompt-templates.ts +0 -30
  5. package/src/config/settings-schema.ts +68 -36
  6. package/src/config/settings.ts +1 -1
  7. package/src/edit/index.ts +1 -53
  8. package/src/edit/line-hash.ts +0 -53
  9. package/src/edit/modes/atom.ts +82 -47
  10. package/src/edit/modes/hashline.ts +6 -8
  11. package/src/edit/renderer.ts +6 -8
  12. package/src/edit/streaming.ts +90 -114
  13. package/src/export/html/template.generated.ts +1 -1
  14. package/src/export/html/template.js +10 -15
  15. package/src/internal-urls/docs-index.generated.ts +1 -2
  16. package/src/modes/components/session-observer-overlay.ts +635 -295
  17. package/src/modes/components/settings-defs.ts +1 -5
  18. package/src/modes/components/tool-execution.ts +2 -5
  19. package/src/modes/controllers/btw-controller.ts +17 -105
  20. package/src/modes/controllers/command-controller.ts +16 -5
  21. package/src/modes/controllers/selector-controller.ts +32 -19
  22. package/src/modes/controllers/todo-command-controller.ts +537 -0
  23. package/src/modes/interactive-mode.ts +45 -10
  24. package/src/modes/types.ts +3 -0
  25. package/src/modes/utils/ui-helpers.ts +17 -0
  26. package/src/prompts/system/irc-incoming.md +8 -0
  27. package/src/prompts/system/subagent-system-prompt.md +8 -0
  28. package/src/prompts/tools/ast-grep.md +1 -1
  29. package/src/prompts/tools/atom.md +37 -26
  30. package/src/prompts/tools/bash.md +2 -2
  31. package/src/prompts/tools/grep.md +2 -5
  32. package/src/prompts/tools/irc.md +49 -0
  33. package/src/prompts/tools/job.md +11 -0
  34. package/src/prompts/tools/read.md +12 -13
  35. package/src/prompts/tools/task.md +1 -1
  36. package/src/prompts/tools/todo-write.md +14 -5
  37. package/src/registry/agent-registry.ts +139 -0
  38. package/src/sdk.ts +35 -0
  39. package/src/session/agent-session.ts +226 -6
  40. package/src/session/session-manager.ts +13 -0
  41. package/src/session/session-storage.ts +4 -0
  42. package/src/session/streaming-output.ts +1 -1
  43. package/src/slash-commands/builtin-registry.ts +32 -0
  44. package/src/task/executor.ts +14 -0
  45. package/src/tools/bash.ts +1 -1
  46. package/src/tools/fetch.ts +18 -6
  47. package/src/tools/fs-cache-invalidation.ts +0 -5
  48. package/src/tools/grep.ts +4 -124
  49. package/src/tools/index.ts +12 -6
  50. package/src/tools/irc.ts +258 -0
  51. package/src/tools/job.ts +489 -0
  52. package/src/tools/match-line-format.ts +7 -6
  53. package/src/tools/output-meta.ts +1 -1
  54. package/src/tools/read.ts +36 -126
  55. package/src/tools/renderers.ts +2 -0
  56. package/src/tools/todo-write.ts +243 -12
  57. package/src/utils/edit-mode.ts +1 -2
  58. package/src/utils/file-display-mode.ts +0 -3
  59. package/src/web/search/index.ts +2 -2
  60. package/src/web/search/provider.ts +3 -0
  61. package/src/web/search/providers/searxng.ts +238 -0
  62. package/src/web/search/types.ts +3 -1
  63. package/src/cli/read-cli.ts +0 -67
  64. package/src/commands/read.ts +0 -33
  65. package/src/edit/modes/chunk.ts +0 -832
  66. package/src/prompts/tools/cancel-job.md +0 -5
  67. package/src/prompts/tools/chunk-edit.md +0 -158
  68. package/src/prompts/tools/poll.md +0 -5
  69. package/src/prompts/tools/read-chunk.md +0 -73
  70. package/src/tools/cancel-job.ts +0 -95
  71. package/src/tools/poll-tool.ts +0 -173
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified Web Search Tool
3
3
  *
4
- * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Tavily, Kagi, Z.AI, and Synthetic
4
+ * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Tavily, Kagi, Z.AI, SearXNG, and Synthetic
5
5
  * providers with provider-specific parameters exposed conditionally.
6
6
  *
7
7
  */
@@ -202,7 +202,7 @@ export async function runSearchQuery(
202
202
  /**
203
203
  * Web search tool implementation.
204
204
  *
205
- * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, and Synthetic providers with automatic fallback.
205
+ * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, SearXNG, and Synthetic providers with automatic fallback.
206
206
  * Session is accepted for interface consistency but not used.
207
207
  */
208
208
  export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
@@ -9,6 +9,7 @@ import { KagiProvider } from "./providers/kagi";
9
9
  import { KimiProvider } from "./providers/kimi";
10
10
  import { ParallelProvider } from "./providers/parallel";
11
11
  import { PerplexityProvider } from "./providers/perplexity";
12
+ import { SearXNGProvider } from "./providers/searxng";
12
13
  import { SyntheticProvider } from "./providers/synthetic";
13
14
  import { TavilyProvider } from "./providers/tavily";
14
15
  import { ZaiProvider } from "./providers/zai";
@@ -31,6 +32,7 @@ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
31
32
  parallel: new ParallelProvider(),
32
33
  kagi: new KagiProvider(),
33
34
  synthetic: new SyntheticProvider(),
35
+ searxng: new SearXNGProvider(),
34
36
  } as const;
35
37
 
36
38
  export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
@@ -47,6 +49,7 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
47
49
  "parallel",
48
50
  "kagi",
49
51
  "synthetic",
52
+ "searxng",
50
53
  ];
51
54
 
52
55
  export function getSearchProvider(provider: SearchProviderId): SearchProvider {
@@ -0,0 +1,238 @@
1
+ /**
2
+ * SearXNG Web Search Provider
3
+ *
4
+ * Calls a SearXNG instance's JSON search API and maps results into the unified
5
+ * SearchResponse shape used by the web search tool.
6
+ *
7
+ * SearXNG is a free, open-source metasearch engine that aggregates results from
8
+ * multiple sources without tracking users. It supports self-hosted instances
9
+ * and various authentication methods (bearer token, basic auth, or none).
10
+ *
11
+ * Configuration via settings:
12
+ * searxng.endpoint - Base URL of the SearXNG instance (e.g. https://searx.example.org)
13
+ * searxng.token - Optional bearer token for authentication
14
+ * searxng.categories - Optional comma-separated categories filter
15
+ * searxng.language - Optional language code (e.g. en, zh-CN)
16
+ *
17
+ * Environment variable fallbacks:
18
+ * SEARXNG_ENDPOINT - Base URL of the SearXNG instance
19
+ * SEARXNG_TOKEN - Optional bearer token
20
+ *
21
+ * Reference: https://docs.searxng.org/dev/search_api.html
22
+ */
23
+
24
+ import { settings } from "../../../config/settings";
25
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
26
+ import { SearchProviderError } from "../../../web/search/types";
27
+ import { clampNumResults, dateToAgeSeconds } from "../utils";
28
+ import type { SearchParams } from "./base";
29
+ import { SearchProvider } from "./base";
30
+
31
+ const DEFAULT_NUM_RESULTS = 10;
32
+ const MAX_NUM_RESULTS = 20;
33
+
34
+ /** Map our recency filter to SearXNG time_range parameter.
35
+ * SearXNG only supports day/month/year, so week maps to month. */
36
+ const RECENCY_MAP: Record<"day" | "week" | "month" | "year", string> = {
37
+ day: "day",
38
+ week: "month",
39
+ month: "month",
40
+ year: "year",
41
+ };
42
+
43
+ /** SearXNG JSON API response types */
44
+ interface SearXNGResult {
45
+ title?: string;
46
+ url?: string;
47
+ content?: string;
48
+ engine?: string;
49
+ publishedDate?: string;
50
+ /** SearXNG sometimes uses publishedDate, sometimes just date */
51
+ published_date?: string;
52
+ score?: number;
53
+ }
54
+
55
+ interface SearXNGResponse {
56
+ query?: string;
57
+ number_of_results?: number;
58
+ results?: SearXNGResult[];
59
+ suggestions?: string[];
60
+ corrections?: string[];
61
+ unresponsive_engines?: Array<[string, string]>;
62
+ }
63
+
64
+ /** Find SearXNG endpoint from settings or environment. */
65
+ function findEndpoint(): string | null {
66
+ try {
67
+ const endpoint = settings.get("searxng.endpoint");
68
+ if (endpoint) return endpoint;
69
+ } catch {
70
+ // Settings not initialized yet
71
+ }
72
+ return process.env.SEARXNG_ENDPOINT ?? null;
73
+ }
74
+
75
+ /** Find SearXNG bearer token from settings or environment. */
76
+ function findToken(): string | null {
77
+ try {
78
+ const token = settings.get("searxng.token");
79
+ if (token) return token;
80
+ } catch {
81
+ // Settings not initialized yet
82
+ }
83
+ return process.env.SEARXNG_TOKEN ?? null;
84
+ }
85
+
86
+ /** Build the search URL and headers for a SearXNG request */
87
+ function buildRequest(
88
+ endpoint: string,
89
+ params: {
90
+ query: string;
91
+ num_results?: number;
92
+ recency?: "day" | "week" | "month" | "year";
93
+ categories?: string;
94
+ language?: string;
95
+ signal?: AbortSignal;
96
+ },
97
+ token: string | null,
98
+ ): { url: URL; headers: Record<string, string> } {
99
+ const base = endpoint.replace(/\/+$/, "");
100
+ const url = new URL(`${base}/search`);
101
+
102
+ url.searchParams.set("q", params.query);
103
+ url.searchParams.set("format", "json");
104
+
105
+ if (params.num_results) {
106
+ url.searchParams.set("pageno", "1");
107
+ }
108
+
109
+ if (params.recency) {
110
+ url.searchParams.set("time_range", RECENCY_MAP[params.recency]);
111
+ }
112
+
113
+ if (params.categories) {
114
+ url.searchParams.set("categories", params.categories);
115
+ }
116
+
117
+ if (params.language) {
118
+ url.searchParams.set("language", params.language);
119
+ }
120
+
121
+ const headers: Record<string, string> = {
122
+ Accept: "application/json",
123
+ };
124
+
125
+ if (token) {
126
+ headers.Authorization = `Bearer ${token}`;
127
+ }
128
+
129
+ return { url, headers };
130
+ }
131
+
132
+ async function callSearXNGSearch(
133
+ endpoint: string,
134
+ params: {
135
+ query: string;
136
+ num_results?: number;
137
+ recency?: "day" | "week" | "month" | "year";
138
+ categories?: string;
139
+ language?: string;
140
+ signal?: AbortSignal;
141
+ },
142
+ token: string | null,
143
+ ): Promise<SearXNGResponse> {
144
+ const { url, headers } = buildRequest(endpoint, params, token);
145
+
146
+ const response = await fetch(url, {
147
+ headers,
148
+ signal: params.signal,
149
+ });
150
+
151
+ if (!response.ok) {
152
+ const errorText = await response.text();
153
+ throw new SearchProviderError("searxng", `SearXNG API error (${response.status}): ${errorText}`, response.status);
154
+ }
155
+
156
+ return (await response.json()) as SearXNGResponse;
157
+ }
158
+
159
+ /** Execute SearXNG web search. */
160
+ export async function searchSearXNG(params: {
161
+ query: string;
162
+ num_results?: number;
163
+ recency?: "day" | "week" | "month" | "year";
164
+ signal?: AbortSignal;
165
+ }): Promise<SearchResponse> {
166
+ const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
167
+
168
+ const endpoint = findEndpoint();
169
+ if (!endpoint) {
170
+ throw new Error(
171
+ "SearXNG endpoint not configured. Set searxng.endpoint in settings or SEARXNG_ENDPOINT in environment.",
172
+ );
173
+ }
174
+
175
+ const token = findToken();
176
+
177
+ let categories: string | undefined;
178
+ let language: string | undefined;
179
+ try {
180
+ categories = settings.get("searxng.categories") ?? undefined;
181
+ language = settings.get("searxng.language") ?? undefined;
182
+ } catch {
183
+ // Settings not initialized yet
184
+ }
185
+
186
+ const response = await callSearXNGSearch(
187
+ endpoint,
188
+ {
189
+ ...params,
190
+ categories,
191
+ language,
192
+ },
193
+ token,
194
+ );
195
+
196
+ const sources: SearchSource[] = [];
197
+
198
+ for (const result of response.results ?? []) {
199
+ if (!result.url) continue;
200
+ const publishedDate = result.publishedDate ?? result.published_date;
201
+ sources.push({
202
+ title: result.title ?? result.url,
203
+ url: result.url,
204
+ snippet: result.content?.trim() || undefined,
205
+ publishedDate: publishedDate ?? undefined,
206
+ ageSeconds: dateToAgeSeconds(publishedDate),
207
+ });
208
+ }
209
+
210
+ return {
211
+ provider: "searxng",
212
+ sources: sources.slice(0, numResults),
213
+ relatedQuestions: response.suggestions?.length ? response.suggestions : undefined,
214
+ };
215
+ }
216
+
217
+ /** Search provider for SearXNG web search. */
218
+ export class SearXNGProvider extends SearchProvider {
219
+ readonly id = "searxng";
220
+ readonly label = "SearXNG";
221
+
222
+ isAvailable() {
223
+ try {
224
+ return !!findEndpoint();
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+
230
+ search(params: SearchParams): Promise<SearchResponse> {
231
+ return searchSearXNG({
232
+ query: params.query,
233
+ num_results: params.numSearchResults ?? params.limit,
234
+ recency: params.recency,
235
+ signal: params.signal,
236
+ });
237
+ }
238
+ }
@@ -18,7 +18,8 @@ export type SearchProviderId =
18
18
  | "tavily"
19
19
  | "parallel"
20
20
  | "kagi"
21
- | "synthetic";
21
+ | "synthetic"
22
+ | "searxng";
22
23
 
23
24
  export function isSearchProviderId(value: string): value is SearchProviderId {
24
25
  return [
@@ -35,6 +36,7 @@ export function isSearchProviderId(value: string): value is SearchProviderId {
35
36
  "parallel",
36
37
  "kagi",
37
38
  "synthetic",
39
+ "searxng",
38
40
  ].includes(value);
39
41
  }
40
42
 
@@ -1,67 +0,0 @@
1
- /**
2
- * Read CLI command handler.
3
- *
4
- * Handles `omp read` subcommand — emits chunk-mode read output for files,
5
- * and delegates URL reads through the read tool pipeline.
6
- */
7
- import * as path from "node:path";
8
- import chalk from "chalk";
9
- import { Settings } from "../config/settings";
10
- import { formatChunkedRead, resolveAnchorStyle } from "../edit/modes/chunk";
11
- import { getLanguageFromPath } from "../modes/theme/theme";
12
- import type { ToolSession } from "../tools";
13
- import { parseReadUrlTarget } from "../tools/fetch";
14
- import { ReadTool } from "../tools/read";
15
-
16
- export interface ReadCommandArgs {
17
- path: string;
18
- sel?: string;
19
- }
20
-
21
- function createCliReadSession(cwd: string, settings: Settings): ToolSession {
22
- return {
23
- cwd,
24
- hasUI: false,
25
- hasEditTool: true,
26
- getSessionFile: () => null,
27
- getSessionSpawns: () => null,
28
- settings,
29
- };
30
- }
31
-
32
- export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
33
- const cwd = process.cwd();
34
- const parsedUrlTarget = parseReadUrlTarget(cmd.path, cmd.sel);
35
- if (parsedUrlTarget) {
36
- const settings = await Settings.init({ cwd });
37
- const tool = new ReadTool(createCliReadSession(cwd, settings));
38
- const result = await tool.execute("cli-read", { path: cmd.path, sel: cmd.sel });
39
- const text = result.content.find((content): content is { type: "text"; text: string } => content.type === "text");
40
- console.log(text?.text ?? "");
41
- return;
42
- }
43
-
44
- const filePath = path.resolve(cmd.path);
45
- const file = Bun.file(filePath);
46
- if (!(await file.exists())) {
47
- console.error(chalk.red(`Error: File not found: ${cmd.path}`));
48
- process.exit(1);
49
- }
50
-
51
- const readPath = cmd.sel ? `${filePath}:${cmd.sel}` : filePath;
52
- const language = getLanguageFromPath(filePath);
53
-
54
- try {
55
- const result = await formatChunkedRead({
56
- filePath,
57
- readPath,
58
- cwd,
59
- language,
60
- anchorStyle: resolveAnchorStyle(),
61
- });
62
- console.log(result.text);
63
- } catch (err) {
64
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
65
- process.exit(1);
66
- }
67
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Chunk-mode read tool.
3
- */
4
- import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
- import { type ReadCommandArgs, runReadCommand } from "../cli/read-cli";
6
- import { initTheme } from "../modes/theme/theme";
7
-
8
- export default class Read extends Command {
9
- static description = "Read a file as a chunk tree";
10
-
11
- static args = {
12
- path: Args.string({ description: "File path to read", required: true }),
13
- };
14
-
15
- static flags = {
16
- sel: Flags.string({
17
- char: "s",
18
- description: "Chunk selector or line range (e.g. class_Foo.fn_bar, L10-L20)",
19
- }),
20
- };
21
-
22
- async run(): Promise<void> {
23
- const { args, flags } = await this.parse(Read);
24
-
25
- const cmd: ReadCommandArgs = {
26
- path: args.path ?? "",
27
- sel: flags.sel,
28
- };
29
-
30
- await initTheme();
31
- await runReadCommand(cmd);
32
- }
33
- }