@oh-my-pi/pi-coding-agent 3.13.1337 → 3.15.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/docs/theme.md +38 -5
  3. package/examples/sdk/11-sessions.ts +2 -2
  4. package/package.json +7 -4
  5. package/src/cli/file-processor.ts +51 -2
  6. package/src/cli/plugin-cli.ts +25 -19
  7. package/src/cli/update-cli.ts +4 -3
  8. package/src/core/agent-session.ts +31 -4
  9. package/src/core/compaction/branch-summarization.ts +4 -32
  10. package/src/core/compaction/compaction.ts +6 -84
  11. package/src/core/compaction/utils.ts +2 -3
  12. package/src/core/custom-tools/types.ts +2 -0
  13. package/src/core/export-html/index.ts +1 -1
  14. package/src/core/hooks/index.ts +1 -1
  15. package/src/core/hooks/tool-wrapper.ts +0 -1
  16. package/src/core/hooks/types.ts +2 -2
  17. package/src/core/plugins/doctor.ts +9 -1
  18. package/src/core/sdk.ts +2 -1
  19. package/src/core/session-manager.ts +552 -41
  20. package/src/core/settings-manager.ts +174 -0
  21. package/src/core/system-prompt.ts +9 -14
  22. package/src/core/title-generator.ts +2 -8
  23. package/src/core/tools/ask.ts +19 -37
  24. package/src/core/tools/bash.ts +2 -37
  25. package/src/core/tools/edit.ts +2 -9
  26. package/src/core/tools/exa/render.ts +52 -48
  27. package/src/core/tools/find.ts +10 -8
  28. package/src/core/tools/grep.ts +45 -17
  29. package/src/core/tools/ls.ts +22 -2
  30. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  31. package/src/core/tools/lsp/clients/index.ts +49 -0
  32. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  33. package/src/core/tools/lsp/config.ts +3 -0
  34. package/src/core/tools/lsp/index.ts +107 -55
  35. package/src/core/tools/lsp/render.ts +192 -79
  36. package/src/core/tools/lsp/types.ts +27 -0
  37. package/src/core/tools/lsp/utils.ts +62 -22
  38. package/src/core/tools/notebook.ts +9 -1
  39. package/src/core/tools/output.ts +37 -14
  40. package/src/core/tools/read.ts +349 -34
  41. package/src/core/tools/renderers.ts +290 -89
  42. package/src/core/tools/review.ts +12 -5
  43. package/src/core/tools/task/agents.ts +5 -5
  44. package/src/core/tools/task/commands.ts +3 -3
  45. package/src/core/tools/task/executor.ts +33 -1
  46. package/src/core/tools/task/index.ts +93 -6
  47. package/src/core/tools/task/render.ts +147 -66
  48. package/src/core/tools/task/types.ts +14 -9
  49. package/src/core/tools/web-fetch.ts +242 -103
  50. package/src/core/tools/web-search/index.ts +64 -20
  51. package/src/core/tools/web-search/providers/exa.ts +68 -172
  52. package/src/core/tools/web-search/render.ts +264 -74
  53. package/src/core/tools/write.ts +2 -8
  54. package/src/main.ts +10 -6
  55. package/src/modes/cleanup.ts +23 -0
  56. package/src/modes/index.ts +9 -4
  57. package/src/modes/interactive/components/bash-execution.ts +6 -3
  58. package/src/modes/interactive/components/branch-summary-message.ts +1 -1
  59. package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  60. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  61. package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
  62. package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
  63. package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
  64. package/src/modes/interactive/components/hook-message.ts +2 -2
  65. package/src/modes/interactive/components/hook-selector.ts +1 -1
  66. package/src/modes/interactive/components/model-selector.ts +22 -9
  67. package/src/modes/interactive/components/oauth-selector.ts +20 -4
  68. package/src/modes/interactive/components/plugin-settings.ts +4 -2
  69. package/src/modes/interactive/components/session-selector.ts +9 -6
  70. package/src/modes/interactive/components/settings-defs.ts +285 -1
  71. package/src/modes/interactive/components/settings-selector.ts +176 -3
  72. package/src/modes/interactive/components/status-line/index.ts +4 -0
  73. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  74. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  75. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  76. package/src/modes/interactive/components/status-line/types.ts +81 -0
  77. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  78. package/src/modes/interactive/components/status-line.ts +169 -233
  79. package/src/modes/interactive/components/tool-execution.ts +446 -211
  80. package/src/modes/interactive/components/tree-selector.ts +17 -6
  81. package/src/modes/interactive/components/ttsr-notification.ts +4 -4
  82. package/src/modes/interactive/components/welcome.ts +27 -19
  83. package/src/modes/interactive/interactive-mode.ts +98 -13
  84. package/src/modes/interactive/theme/dark.json +3 -2
  85. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  86. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  87. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  88. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  89. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  90. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  91. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  92. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  93. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  94. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  95. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  96. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  97. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  98. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  99. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  100. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  101. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  102. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  103. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  104. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  105. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  106. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  107. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  108. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  109. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  110. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  111. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  112. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  113. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  114. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  115. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  116. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  117. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  118. package/src/modes/interactive/theme/light.json +3 -2
  119. package/src/modes/interactive/theme/theme-schema.json +120 -4
  120. package/src/modes/interactive/theme/theme.ts +1228 -14
  121. package/src/prompts/branch-summary-preamble.md +3 -0
  122. package/src/prompts/branch-summary.md +28 -0
  123. package/src/prompts/compaction-summary.md +34 -0
  124. package/src/prompts/compaction-turn-prefix.md +16 -0
  125. package/src/prompts/compaction-update-summary.md +41 -0
  126. package/src/prompts/init.md +30 -0
  127. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  128. package/src/prompts/summarization-system.md +3 -0
  129. package/src/prompts/system-prompt.md +27 -0
  130. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  131. package/src/prompts/title-system.md +8 -0
  132. package/src/prompts/tools/ask.md +24 -0
  133. package/src/prompts/tools/bash.md +23 -0
  134. package/src/prompts/tools/edit.md +9 -0
  135. package/src/prompts/tools/find.md +6 -0
  136. package/src/prompts/tools/grep.md +12 -0
  137. package/src/prompts/tools/lsp.md +14 -0
  138. package/src/prompts/tools/output.md +23 -0
  139. package/src/prompts/tools/read.md +25 -0
  140. package/src/prompts/tools/web-fetch.md +8 -0
  141. package/src/prompts/tools/web-search.md +10 -0
  142. package/src/prompts/tools/write.md +10 -0
  143. package/src/commands/init.md +0 -20
  144. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  145. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  146. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  147. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  148. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  149. /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
@@ -1,19 +1,22 @@
1
1
  /**
2
2
  * Exa Web Search Provider
3
3
  *
4
- * High-quality neural search via Exa MCP.
4
+ * High-quality neural search via Exa Search API.
5
5
  * Returns structured search results with optional content extraction.
6
6
  */
7
7
 
8
- import * as os from "node:os";
9
8
  import type { WebSearchResponse, WebSearchSource } from "../types";
10
9
 
11
- const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
10
+ const EXA_API_URL = "https://api.exa.ai/search";
11
+
12
+ type ExaSearchType = "neural" | "fast" | "auto" | "deep";
13
+
14
+ type ExaSearchParamType = ExaSearchType | "keyword";
12
15
 
13
16
  export interface ExaSearchParams {
14
17
  query: string;
15
18
  num_results?: number;
16
- type?: "neural" | "keyword" | "auto";
19
+ type?: ExaSearchParamType;
17
20
  include_domains?: string[];
18
21
  exclude_domains?: string[];
19
22
  start_published_date?: string;
@@ -29,9 +32,13 @@ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
29
32
 
30
33
  const content = await file.text();
31
34
  for (const line of content.split("\n")) {
32
- const trimmed = line.trim();
35
+ let trimmed = line.trim();
33
36
  if (!trimmed || trimmed.startsWith("#")) continue;
34
37
 
38
+ if (trimmed.startsWith("export ")) {
39
+ trimmed = trimmed.slice("export ".length).trim();
40
+ }
41
+
35
42
  const eqIndex = trimmed.indexOf("=");
36
43
  if (eqIndex === -1) continue;
37
44
 
@@ -50,6 +57,10 @@ async function parseEnvFile(filePath: string): Promise<Record<string, string>> {
50
57
  return result;
51
58
  }
52
59
 
60
+ function getHomeDir(): string | null {
61
+ return process.env.HOME ?? process.env.USERPROFILE ?? null;
62
+ }
63
+
53
64
  /** Find EXA_API_KEY from environment or .env files */
54
65
  export async function findApiKey(): Promise<string | null> {
55
66
  // 1. Check environment variable
@@ -64,171 +75,80 @@ export async function findApiKey(): Promise<string | null> {
64
75
  }
65
76
 
66
77
  // 3. Check ~/.env
67
- const homeEnv = await parseEnvFile(`${os.homedir()}/.env`);
68
- if (homeEnv.EXA_API_KEY) {
69
- return homeEnv.EXA_API_KEY;
70
- }
71
-
72
- return null;
73
- }
74
-
75
- /** Parse SSE response format */
76
- function parseSSE(text: string): unknown {
77
- const lines = text.split("\n");
78
- for (const line of lines) {
79
- if (line.startsWith("data: ")) {
80
- const data = line.slice(6).trim();
81
- if (data === "[DONE]") continue;
82
- try {
83
- return JSON.parse(data);
84
- } catch {
85
- // Try next line
86
- }
78
+ const homeDir = getHomeDir();
79
+ if (homeDir) {
80
+ const homeEnv = await parseEnvFile(`${homeDir}/.env`);
81
+ if (homeEnv.EXA_API_KEY) {
82
+ return homeEnv.EXA_API_KEY;
87
83
  }
88
84
  }
89
- // Fallback: try parsing entire response as JSON
90
- try {
91
- return JSON.parse(text);
92
- } catch {
93
- return null;
94
- }
95
- }
96
85
 
97
- interface MCPCallResponse {
98
- result?: {
99
- content?: Array<{ type: string; text?: string }>;
100
- };
101
- error?: {
102
- code: number;
103
- message: string;
104
- };
86
+ return null;
105
87
  }
106
88
 
107
89
  interface ExaSearchResult {
108
- title?: string;
109
- url?: string;
110
- author?: string;
111
- publishedDate?: string;
112
- text?: string;
113
- highlights?: string[];
90
+ title?: string | null;
91
+ url?: string | null;
92
+ author?: string | null;
93
+ publishedDate?: string | null;
94
+ text?: string | null;
95
+ highlights?: string[] | null;
114
96
  }
115
97
 
116
98
  interface ExaSearchResponse {
99
+ requestId?: string;
100
+ resolvedSearchType?: string;
117
101
  results?: ExaSearchResult[];
118
102
  costDollars?: { total: number };
119
103
  searchTime?: number;
120
104
  }
121
105
 
122
- /** Call Exa MCP API */
123
- async function callExaMCP(apiKey: string, toolName: string, args: Record<string, unknown>): Promise<MCPCallResponse> {
124
- const url = `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolName)}`;
125
-
126
- const body = {
127
- jsonrpc: "2.0",
128
- id: Math.random().toString(36).slice(2),
129
- method: "tools/call",
130
- params: {
131
- name: toolName,
132
- arguments: args,
133
- },
106
+ function normalizeSearchType(type: ExaSearchParamType | undefined): ExaSearchType {
107
+ if (!type) return "auto";
108
+ if (type === "keyword") return "fast";
109
+ return type;
110
+ }
111
+
112
+ /** Call Exa Search API */
113
+ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<ExaSearchResponse> {
114
+ const body: Record<string, unknown> = {
115
+ query: params.query,
116
+ numResults: params.num_results ?? 10,
117
+ type: normalizeSearchType(params.type),
134
118
  };
135
119
 
136
- const response = await fetch(url, {
120
+ if (params.include_domains?.length) {
121
+ body.includeDomains = params.include_domains;
122
+ }
123
+ if (params.exclude_domains?.length) {
124
+ body.excludeDomains = params.exclude_domains;
125
+ }
126
+ if (params.start_published_date) {
127
+ body.startPublishedDate = params.start_published_date;
128
+ }
129
+ if (params.end_published_date) {
130
+ body.endPublishedDate = params.end_published_date;
131
+ }
132
+
133
+ const response = await fetch(EXA_API_URL, {
137
134
  method: "POST",
138
135
  headers: {
139
136
  "Content-Type": "application/json",
140
- Accept: "application/json, text/event-stream",
137
+ "x-api-key": apiKey,
141
138
  },
142
139
  body: JSON.stringify(body),
143
140
  });
144
141
 
145
142
  if (!response.ok) {
146
143
  const errorText = await response.text();
147
- throw new Error(`Exa MCP error (${response.status}): ${errorText}`);
148
- }
149
-
150
- const text = await response.text();
151
- const result = parseSSE(text);
152
-
153
- if (!result) {
154
- throw new Error("Failed to parse Exa MCP response");
155
- }
156
-
157
- return result as MCPCallResponse;
158
- }
159
-
160
- /** Parse MCP response content into ExaSearchResponse */
161
- function parseMCPContent(content: Array<{ type: string; text?: string }>): ExaSearchResponse | null {
162
- for (const block of content) {
163
- if (block.type === "text" && block.text) {
164
- // Try to parse as JSON first
165
- try {
166
- return JSON.parse(block.text) as ExaSearchResponse;
167
- } catch {
168
- // Parse markdown format
169
- return parseExaMarkdown(block.text);
170
- }
171
- }
172
- }
173
- return null;
174
- }
175
-
176
- /** Parse Exa markdown format into ExaSearchResponse */
177
- function parseExaMarkdown(text: string): ExaSearchResponse | null {
178
- const results: ExaSearchResult[] = [];
179
- const lines = text.split("\n");
180
- let currentResult: Partial<ExaSearchResult> | null = null;
181
-
182
- for (const line of lines) {
183
- const trimmed = line.trim();
184
-
185
- // Match result header: ## Title
186
- if (trimmed.startsWith("## ")) {
187
- if (currentResult?.title) {
188
- results.push(currentResult as ExaSearchResult);
189
- }
190
- currentResult = { title: trimmed.slice(3).trim() };
191
- continue;
192
- }
193
-
194
- if (!currentResult) continue;
195
-
196
- // Match URL: **URL:** ...
197
- if (trimmed.startsWith("**URL:**")) {
198
- currentResult.url = trimmed.slice(8).trim();
199
- continue;
200
- }
201
-
202
- // Match Author: **Author:** ...
203
- if (trimmed.startsWith("**Author:**")) {
204
- currentResult.author = trimmed.slice(11).trim();
205
- continue;
206
- }
207
-
208
- // Match Published Date: **Published Date:** ...
209
- if (trimmed.startsWith("**Published Date:**")) {
210
- currentResult.publishedDate = trimmed.slice(19).trim();
211
- continue;
212
- }
213
-
214
- // Match Text: **Text:** ...
215
- if (trimmed.startsWith("**Text:**")) {
216
- currentResult.text = trimmed.slice(9).trim();
217
- }
144
+ throw new Error(`Exa API error (${response.status}): ${errorText}`);
218
145
  }
219
146
 
220
- // Add last result
221
- if (currentResult?.title) {
222
- results.push(currentResult as ExaSearchResult);
223
- }
224
-
225
- if (results.length === 0) return null;
226
-
227
- return { results };
147
+ return response.json() as Promise<ExaSearchResponse>;
228
148
  }
229
149
 
230
150
  /** Calculate age in seconds from ISO date string */
231
- function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
151
+ function dateToAgeSeconds(dateStr: string | null | undefined): number | undefined {
232
152
  if (!dateStr) return undefined;
233
153
  try {
234
154
  const date = new Date(dateStr);
@@ -246,46 +166,21 @@ export async function searchExa(params: ExaSearchParams): Promise<WebSearchRespo
246
166
  throw new Error("EXA_API_KEY not found. Set it in environment or .env file.");
247
167
  }
248
168
 
249
- const args: Record<string, unknown> = {
250
- query: params.query,
251
- numResults: params.num_results ?? 10,
252
- type: params.type ?? "auto",
253
- };
254
-
255
- if (params.include_domains?.length) {
256
- args.include_domains = params.include_domains;
257
- }
258
- if (params.exclude_domains?.length) {
259
- args.exclude_domains = params.exclude_domains;
260
- }
261
- if (params.start_published_date) {
262
- args.start_published_date = params.start_published_date;
263
- }
264
- if (params.end_published_date) {
265
- args.end_published_date = params.end_published_date;
266
- }
267
-
268
- const response = await callExaMCP(apiKey, "web_search_exa", args);
269
-
270
- if (response.error) {
271
- throw new Error(`Exa MCP error: ${response.error.message}`);
272
- }
273
-
274
- const exaResponse = response.result?.content ? parseMCPContent(response.result.content) : null;
169
+ const response = await callExaSearch(apiKey, params);
275
170
 
276
171
  // Convert to unified WebSearchResponse
277
172
  const sources: WebSearchSource[] = [];
278
173
 
279
- if (exaResponse?.results) {
280
- for (const result of exaResponse.results) {
174
+ if (response.results) {
175
+ for (const result of response.results) {
281
176
  if (!result.url) continue;
282
177
  sources.push({
283
178
  title: result.title ?? result.url,
284
179
  url: result.url,
285
- snippet: result.text ?? result.highlights?.join(" "),
286
- publishedDate: result.publishedDate,
287
- ageSeconds: dateToAgeSeconds(result.publishedDate),
288
- author: result.author,
180
+ snippet: result.text ?? result.highlights?.join(" ") ?? undefined,
181
+ publishedDate: result.publishedDate ?? undefined,
182
+ ageSeconds: dateToAgeSeconds(result.publishedDate ?? undefined),
183
+ author: result.author ?? undefined,
289
184
  });
290
185
  }
291
186
  }
@@ -296,5 +191,6 @@ export async function searchExa(params: ExaSearchParams): Promise<WebSearchRespo
296
191
  return {
297
192
  provider: "exa",
298
193
  sources: limitedSources,
194
+ requestId: response.requestId,
299
195
  };
300
196
  }