@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.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 (143) hide show
  1. package/CHANGELOG.md +199 -49
  2. package/README.md +1 -1
  3. package/docs/config-usage.md +3 -4
  4. package/docs/sdk.md +6 -5
  5. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  6. package/examples/sdk/README.md +1 -1
  7. package/package.json +19 -11
  8. package/src/cli/args.ts +11 -94
  9. package/src/cli/config-cli.ts +1 -1
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/oclif-help.ts +26 -0
  12. package/src/cli/web-search-cli.ts +148 -0
  13. package/src/cli.ts +8 -2
  14. package/src/commands/commit.ts +36 -0
  15. package/src/commands/config.ts +51 -0
  16. package/src/commands/grep.ts +41 -0
  17. package/src/commands/index/index.ts +136 -0
  18. package/src/commands/jupyter.ts +32 -0
  19. package/src/commands/plugin.ts +70 -0
  20. package/src/commands/setup.ts +39 -0
  21. package/src/commands/shell.ts +29 -0
  22. package/src/commands/stats.ts +29 -0
  23. package/src/commands/update.ts +21 -0
  24. package/src/commands/web-search.ts +50 -0
  25. package/src/commit/agentic/index.ts +3 -2
  26. package/src/commit/agentic/tools/analyze-file.ts +1 -3
  27. package/src/commit/git/errors.ts +4 -6
  28. package/src/commit/pipeline.ts +3 -2
  29. package/src/config/keybindings.ts +1 -3
  30. package/src/config/model-registry.ts +89 -162
  31. package/src/config/settings-schema.ts +10 -0
  32. package/src/config.ts +202 -132
  33. package/src/exa/mcp-client.ts +8 -41
  34. package/src/export/html/index.ts +1 -1
  35. package/src/extensibility/extensions/loader.ts +7 -10
  36. package/src/extensibility/extensions/runner.ts +5 -15
  37. package/src/extensibility/extensions/types.ts +1 -1
  38. package/src/extensibility/hooks/runner.ts +6 -9
  39. package/src/index.ts +0 -1
  40. package/src/ipy/kernel.ts +10 -22
  41. package/src/lsp/clients/biome-client.ts +4 -7
  42. package/src/lsp/clients/lsp-linter-client.ts +4 -6
  43. package/src/lsp/index.ts +5 -4
  44. package/src/lsp/utils.ts +18 -0
  45. package/src/main.ts +86 -181
  46. package/src/mcp/json-rpc.ts +2 -2
  47. package/src/mcp/transports/http.ts +12 -49
  48. package/src/modes/components/armin.ts +1 -3
  49. package/src/modes/components/assistant-message.ts +4 -4
  50. package/src/modes/components/bash-execution.ts +5 -3
  51. package/src/modes/components/branch-summary-message.ts +1 -3
  52. package/src/modes/components/compaction-summary-message.ts +1 -3
  53. package/src/modes/components/custom-message.ts +4 -5
  54. package/src/modes/components/extensions/extension-dashboard.ts +10 -16
  55. package/src/modes/components/extensions/extension-list.ts +5 -5
  56. package/src/modes/components/footer.ts +1 -4
  57. package/src/modes/components/hook-editor.ts +7 -32
  58. package/src/modes/components/hook-message.ts +4 -5
  59. package/src/modes/components/model-selector.ts +2 -2
  60. package/src/modes/components/plugin-settings.ts +16 -20
  61. package/src/modes/components/python-execution.ts +5 -5
  62. package/src/modes/components/session-selector.ts +6 -7
  63. package/src/modes/components/settings-defs.ts +49 -40
  64. package/src/modes/components/settings-selector.ts +8 -17
  65. package/src/modes/components/skill-message.ts +1 -3
  66. package/src/modes/components/status-line-segment-editor.ts +1 -3
  67. package/src/modes/components/status-line.ts +1 -3
  68. package/src/modes/components/todo-reminder.ts +5 -7
  69. package/src/modes/components/tree-selector.ts +10 -12
  70. package/src/modes/components/ttsr-notification.ts +1 -3
  71. package/src/modes/components/user-message-selector.ts +2 -4
  72. package/src/modes/components/welcome.ts +6 -18
  73. package/src/modes/controllers/event-controller.ts +1 -0
  74. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  75. package/src/modes/controllers/input-controller.ts +7 -34
  76. package/src/modes/controllers/selector-controller.ts +3 -3
  77. package/src/modes/interactive-mode.ts +27 -1
  78. package/src/modes/rpc/rpc-client.ts +2 -5
  79. package/src/modes/rpc/rpc-mode.ts +2 -2
  80. package/src/modes/theme/theme.ts +2 -6
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +6 -1
  83. package/src/patch/index.ts +1 -4
  84. package/src/prompts/agents/explore.md +1 -0
  85. package/src/prompts/agents/frontmatter.md +2 -1
  86. package/src/prompts/agents/init.md +1 -0
  87. package/src/prompts/agents/plan.md +1 -0
  88. package/src/prompts/agents/reviewer.md +1 -0
  89. package/src/prompts/system/subagent-submit-reminder.md +2 -0
  90. package/src/prompts/system/subagent-system-prompt.md +2 -0
  91. package/src/prompts/system/subagent-user-prompt.md +8 -0
  92. package/src/prompts/system/system-prompt.md +5 -3
  93. package/src/prompts/system/web-search.md +6 -4
  94. package/src/prompts/tools/task.md +216 -163
  95. package/src/sdk.ts +11 -110
  96. package/src/session/agent-session.ts +117 -83
  97. package/src/session/auth-storage.ts +10 -51
  98. package/src/session/messages.ts +17 -3
  99. package/src/session/session-manager.ts +30 -30
  100. package/src/session/streaming-output.ts +1 -1
  101. package/src/ssh/ssh-executor.ts +6 -3
  102. package/src/task/agents.ts +2 -0
  103. package/src/task/discovery.ts +1 -1
  104. package/src/task/executor.ts +5 -10
  105. package/src/task/index.ts +43 -23
  106. package/src/task/render.ts +67 -64
  107. package/src/task/template.ts +17 -34
  108. package/src/task/types.ts +49 -22
  109. package/src/tools/ask.ts +1 -3
  110. package/src/tools/bash.ts +1 -4
  111. package/src/tools/browser.ts +5 -7
  112. package/src/tools/exit-plan-mode.ts +1 -4
  113. package/src/tools/fetch.ts +1 -3
  114. package/src/tools/find.ts +4 -3
  115. package/src/tools/gemini-image.ts +24 -55
  116. package/src/tools/grep.ts +4 -4
  117. package/src/tools/index.ts +12 -14
  118. package/src/tools/notebook.ts +1 -5
  119. package/src/tools/python.ts +4 -3
  120. package/src/tools/read.ts +2 -4
  121. package/src/tools/render-utils.ts +23 -0
  122. package/src/tools/ssh.ts +8 -12
  123. package/src/tools/todo-write.ts +1 -4
  124. package/src/tools/tool-errors.ts +1 -4
  125. package/src/tools/write.ts +1 -3
  126. package/src/utils/external-editor.ts +59 -0
  127. package/src/utils/file-mentions.ts +39 -1
  128. package/src/utils/image-convert.ts +1 -1
  129. package/src/utils/image-resize.ts +4 -4
  130. package/src/web/search/auth.ts +3 -33
  131. package/src/web/search/index.ts +73 -139
  132. package/src/web/search/provider.ts +58 -0
  133. package/src/web/search/providers/anthropic.ts +53 -14
  134. package/src/web/search/providers/base.ts +22 -0
  135. package/src/web/search/providers/codex.ts +38 -16
  136. package/src/web/search/providers/exa.ts +30 -6
  137. package/src/web/search/providers/gemini.ts +56 -20
  138. package/src/web/search/providers/jina.ts +28 -5
  139. package/src/web/search/providers/perplexity.ts +103 -36
  140. package/src/web/search/render.ts +84 -74
  141. package/src/web/search/types.ts +285 -59
  142. package/src/migrations.ts +0 -175
  143. package/src/session/storage-migration.ts +0 -173
package/src/config.ts CHANGED
@@ -2,6 +2,10 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
5
+ import type { TSchema } from "@sinclair/typebox";
6
+ import { Value } from "@sinclair/typebox/value/index.mjs";
7
+ import { Ajv, type ErrorObject, type ValidateFunction } from "ajv";
8
+ import { JSONC, YAML } from "bun";
5
9
  // Embed package.json at build time for config
6
10
  import packageJson from "../package.json" with { type: "json" };
7
11
 
@@ -51,29 +55,205 @@ export function getChangelogPath(): string {
51
55
  // User Config Paths (~/.omp/agent/*)
52
56
  // =============================================================================
53
57
 
54
- /** Get the agent config directory (e.g., ~/.omp/agent/) */
55
- export function getAgentDir(): string {
56
- return $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
58
+ function migrateJsonToYml(jsonPath: string, ymlPath: string) {
59
+ try {
60
+ if (fs.existsSync(ymlPath)) return;
61
+ if (!fs.existsSync(jsonPath)) return;
62
+
63
+ const content = fs.readFileSync(jsonPath, "utf-8");
64
+ const parsed = JSON.parse(content);
65
+ if (!parsed) {
66
+ logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
67
+ return;
68
+ }
69
+ fs.writeFileSync(ymlPath, YAML.stringify(parsed, null, 2));
70
+ } catch (error) {
71
+ logger.warn("migrateJsonToYml: migration failed", { error: String(error) });
72
+ }
57
73
  }
58
74
 
59
- /** Get path to user's custom themes directory */
60
- export function getCustomThemesDir(): string {
61
- return path.join(getAgentDir(), "themes");
75
+ export interface IConfigFile<T> {
76
+ readonly id: string;
77
+ readonly schema: TSchema;
78
+ path?(): string;
79
+ load(): T | null;
80
+ invalidate?(): void;
62
81
  }
63
82
 
64
- /** Get path to models.json */
65
- export function getModelsPath(): string {
66
- return path.join(getAgentDir(), "models.json");
83
+ export class ConfigError extends Error {
84
+ readonly #message: string;
85
+ constructor(
86
+ public readonly id: string,
87
+ public readonly schemaErrors: ErrorObject[] | null | undefined,
88
+ public readonly other?: { err: unknown; stage: string },
89
+ ) {
90
+ let messages: string[] | undefined;
91
+ let cause: any | undefined;
92
+ let klass: string;
93
+
94
+ if (schemaErrors) {
95
+ klass = "Schema";
96
+ messages = schemaErrors.map(e => `${e.instancePath || "root"}: ${e.message}`);
97
+ } else if (other) {
98
+ klass = other.stage;
99
+ if (other.err instanceof Error) {
100
+ messages = [other.err.message];
101
+ cause = other.err;
102
+ } else {
103
+ messages = [String(other.err)];
104
+ }
105
+ } else {
106
+ klass = "Unknown";
107
+ }
108
+
109
+ const title = `Failed to load config file ${id}, ${klass} error:`;
110
+ let message: string;
111
+ switch (messages?.length ?? 0) {
112
+ case 0:
113
+ message = title.slice(0, -1);
114
+ break;
115
+ case 1:
116
+ message = `${title} ${messages![0]}`;
117
+ break;
118
+ default:
119
+ message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
120
+ break;
121
+ }
122
+
123
+ super(message, { cause });
124
+ this.name = "LoadError";
125
+ this.#message = message;
126
+ }
127
+
128
+ get message(): string {
129
+ return this.#message;
130
+ }
131
+
132
+ toString(): string {
133
+ return this.message;
134
+ }
67
135
  }
68
136
 
69
- /** Get path to models.yml (preferred over models.json) */
70
- export function getModelsYamlPath(): string {
71
- return path.join(getAgentDir(), "models.yml");
137
+ export type LoadStatus = "ok" | "error" | "not-found";
138
+
139
+ export type LoadResult<T> =
140
+ | { value?: null; error: ConfigError; status: "error" }
141
+ | { value: T; error?: undefined; status: "ok" }
142
+ | { value?: null; error?: unknown; status: "not-found" };
143
+
144
+ const ajv = new Ajv();
145
+ export class ConfigFile<T> implements IConfigFile<T> {
146
+ readonly #basePath: string;
147
+ #cache?: LoadResult<T>;
148
+ #auxValidate?: (value: T) => void;
149
+
150
+ constructor(
151
+ public readonly id: string,
152
+ public readonly schema: TSchema,
153
+ configPath: string = path.join(getAgentDir(), `${id}.yml`),
154
+ ) {
155
+ this.#basePath = configPath;
156
+ if (configPath.endsWith(".yml")) {
157
+ const jsonPath = `${configPath.slice(0, -4)}.json`;
158
+ migrateJsonToYml(jsonPath, configPath);
159
+ } else if (configPath.endsWith(".yaml")) {
160
+ const jsonPath = `${configPath.slice(0, -5)}.json`;
161
+ migrateJsonToYml(jsonPath, configPath);
162
+ } else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
163
+ // JSON configs are still supported without migration.
164
+ } else {
165
+ throw new Error(`Invalid config file path: ${configPath}`);
166
+ }
167
+ }
168
+
169
+ relocate(path?: string): ConfigFile<T> {
170
+ if (!path || path === this.#basePath) return this;
171
+ const result = new ConfigFile<T>(this.id, this.schema, path);
172
+ result.#auxValidate = this.#auxValidate;
173
+ return result;
174
+ }
175
+
176
+ withValidation(name: string, validate: (value: T) => void): this {
177
+ const prev = this.#auxValidate;
178
+ this.#auxValidate = (value: T) => {
179
+ prev?.(value);
180
+ try {
181
+ validate(value);
182
+ } catch (error) {
183
+ throw new ConfigError(this.id, undefined, { err: error, stage: `Validate(${name})` });
184
+ }
185
+ };
186
+ return this;
187
+ }
188
+
189
+ createDefault() {
190
+ return Value.Default(this.schema, [], undefined) as T;
191
+ }
192
+
193
+ #storeCache(result: LoadResult<T>): LoadResult<T> {
194
+ this.#cache = result;
195
+ return result;
196
+ }
197
+
198
+ tryLoad(): LoadResult<T> {
199
+ if (this.#cache) return this.#cache;
200
+
201
+ try {
202
+ const content = fs.readFileSync(this.path(), "utf-8").trim();
203
+
204
+ let parsed: unknown;
205
+ if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
206
+ parsed = JSONC.parse(content);
207
+ } else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
208
+ parsed = YAML.parse(content);
209
+ } else {
210
+ throw new Error(`Invalid config file path: ${this.#basePath}`);
211
+ }
212
+
213
+ const validate = ajv.compile(this.schema) as ValidateFunction<T>;
214
+ if (!validate(parsed)) {
215
+ const error = new ConfigError(this.id, validate.errors);
216
+ logger.warn("Failed to parse config file", { path: this.path(), error });
217
+ return this.#storeCache({ error, status: "error" });
218
+ }
219
+ return this.#storeCache({ value: parsed, status: "ok" });
220
+ } catch (error) {
221
+ if (!isEnoent(error)) {
222
+ return this.#storeCache({ status: "not-found" });
223
+ }
224
+ logger.warn("Failed to parse config file", { path: this.path(), error });
225
+ return this.#storeCache({
226
+ error: new ConfigError(this.id, undefined, { err: error, stage: "Unexpected" }),
227
+ status: "error",
228
+ });
229
+ }
230
+ }
231
+
232
+ load(): T | null {
233
+ return this.tryLoad().value ?? null;
234
+ }
235
+
236
+ loadOrDefault(): T {
237
+ return this.tryLoad().value ?? this.createDefault();
238
+ }
239
+
240
+ path(): string {
241
+ return this.#basePath;
242
+ }
243
+
244
+ invalidate() {
245
+ this.#cache = undefined;
246
+ }
247
+ }
248
+
249
+ /** Get the agent config directory (e.g., ~/.omp/agent/) */
250
+ export function getAgentDir(): string {
251
+ return $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
72
252
  }
73
253
 
74
- /** Get path to auth.json */
75
- export function getAuthPath(): string {
76
- return path.join(getAgentDir(), "auth.json");
254
+ /** Get path to user's custom themes directory */
255
+ export function getCustomThemesDir(): string {
256
+ return path.join(getAgentDir(), "themes");
77
257
  }
78
258
 
79
259
  /**
@@ -204,75 +384,6 @@ export interface ConfigFileResult<T> {
204
384
  content: T;
205
385
  }
206
386
 
207
- /**
208
- * Read the first existing config file from priority-ordered locations.
209
- *
210
- * @param subpath - Subpath within config dirs (e.g., "settings.json", "models.json")
211
- * @param options - Options for filtering (same as getConfigDirs)
212
- * @returns The parsed content and metadata, or undefined if not found
213
- *
214
- * @example
215
- * const result = readConfigFile<Settings>("settings.json", { project: false });
216
- * if (result) {
217
- * console.log(`Loaded from ${result.path}`);
218
- * console.log(result.content);
219
- * }
220
- */
221
- export async function readConfigFile<T = unknown>(
222
- subpath: string,
223
- options: GetConfigDirsOptions = {},
224
- ): Promise<ConfigFileResult<T> | undefined> {
225
- const dirs = getConfigDirs("", { ...options, existingOnly: false });
226
-
227
- for (const { path: base, source, level } of dirs) {
228
- const filePath = path.join(base, subpath);
229
- try {
230
- const content = await Bun.file(filePath).text();
231
- return {
232
- path: filePath,
233
- source,
234
- level,
235
- content: JSON.parse(content) as T,
236
- };
237
- } catch (error) {
238
- if (isEnoent(error)) continue;
239
- logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
240
- }
241
- }
242
-
243
- return undefined;
244
- }
245
-
246
- /**
247
- * Get all existing config files for a subpath (for merging scenarios).
248
- * Returns in priority order (highest first).
249
- */
250
- export async function readAllConfigFiles<T = unknown>(
251
- subpath: string,
252
- options: GetConfigDirsOptions = {},
253
- ): Promise<ConfigFileResult<T>[]> {
254
- const dirs = getConfigDirs("", { ...options, existingOnly: false });
255
- const results: ConfigFileResult<T>[] = [];
256
-
257
- for (const { path: base, source, level } of dirs) {
258
- const filePath = path.join(base, subpath);
259
- try {
260
- const content = await Bun.file(filePath).text();
261
- results.push({
262
- path: filePath,
263
- source,
264
- level,
265
- content: JSON.parse(content) as T,
266
- });
267
- } catch (error) {
268
- if (isEnoent(error)) continue;
269
- logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
270
- }
271
- }
272
-
273
- return results;
274
- }
275
-
276
387
  /**
277
388
  * Find the first existing config file (for non-JSON files like SYSTEM.md).
278
389
  * Returns just the path, or undefined if not found.
@@ -313,55 +424,12 @@ export function findConfigFileWithMeta(
313
424
  // Walk-Up Config Discovery (for monorepo scenarios)
314
425
  // =============================================================================
315
426
 
316
- async function isDirectory(p: string): Promise<boolean> {
317
- try {
318
- return (await fs.promises.stat(p)).isDirectory();
319
- } catch {
320
- return false;
321
- }
322
- }
323
-
324
- /**
325
- * Find nearest config directory by walking up from cwd.
326
- * Checks all config bases (.omp, .pi, .claude) at each level.
327
- *
328
- * @param subpath - Subpath within config dirs (e.g., "commands", "agents")
329
- * @param cwd - Starting directory
330
- * @returns First existing directory found, or undefined
331
- */
332
- export async function findNearestProjectConfigDir(
333
- subpath: string,
334
- cwd: string = process.cwd(),
335
- ): Promise<ConfigDirEntry | undefined> {
336
- let currentDir = cwd;
337
-
338
- while (true) {
339
- // Check all config bases at this level, in priority order
340
- for (const { base, name } of PROJECT_CONFIG_BASES) {
341
- const candidate = path.join(currentDir, base, subpath);
342
- if (await isDirectory(candidate)) {
343
- return { path: candidate, source: name, level: "project" };
344
- }
345
- }
346
-
347
- // Move up one directory
348
- const parentDir = path.dirname(currentDir);
349
- if (parentDir === currentDir) break; // Reached root
350
- currentDir = parentDir;
351
- }
352
-
353
- return undefined;
354
- }
355
-
356
427
  /**
357
428
  * Find all nearest config directories by walking up from cwd.
358
429
  * Returns one entry per config base (.omp, .pi, .claude) - the nearest one found.
359
430
  * Results are in priority order (highest first).
360
431
  */
361
- export async function findAllNearestProjectConfigDirs(
362
- subpath: string,
363
- cwd: string = process.cwd(),
364
- ): Promise<ConfigDirEntry[]> {
432
+ export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = process.cwd()): ConfigDirEntry[] {
365
433
  const results: ConfigDirEntry[] = [];
366
434
  const foundBases = new Set<string>();
367
435
 
@@ -372,10 +440,12 @@ export async function findAllNearestProjectConfigDirs(
372
440
  if (foundBases.has(name)) continue;
373
441
 
374
442
  const candidate = path.join(currentDir, base, subpath);
375
- if (await isDirectory(candidate)) {
376
- results.push({ path: candidate, source: name, level: "project" });
377
- foundBases.add(name);
378
- }
443
+ try {
444
+ if (fs.statSync(candidate).isDirectory()) {
445
+ results.push({ path: candidate, source: name, level: "project" });
446
+ foundBases.add(name);
447
+ }
448
+ } catch {}
379
449
  }
380
450
 
381
451
  const parentDir = path.dirname(currentDir);
@@ -1,10 +1,4 @@
1
- /**
2
- * Exa MCP Client
3
- *
4
- * Client for interacting with Exa MCP servers.
5
- */
6
- import * as os from "node:os";
7
- import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
1
+ import { $env, logger } from "@oh-my-pi/pi-utils";
8
2
  import type { TSchema } from "@sinclair/typebox";
9
3
  import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types";
10
4
  import { callMCP } from "../mcp/json-rpc";
@@ -19,32 +13,8 @@ import type {
19
13
  } from "./types";
20
14
 
21
15
  /** Find EXA_API_KEY from Bun.env or .env files */
22
- export async function findApiKey(): Promise<string | null> {
23
- // Check Bun.env first
24
- if ($env.EXA_API_KEY) {
25
- return $env.EXA_API_KEY;
26
- }
27
-
28
- // Try loading from .env files in cwd and home
29
- const cwd = process.cwd();
30
- const home = os.homedir();
31
-
32
- for (const dir of [cwd, home]) {
33
- const envPath = `${dir}/.env`;
34
- try {
35
- const content = await Bun.file(envPath).text();
36
- const match = content.match(/^EXA_API_KEY=(.+)$/m);
37
- if (match?.[1]) {
38
- return match[1].trim().replace(/^["']|["']$/g, "");
39
- }
40
- } catch (err) {
41
- if (!isEnoent(err)) {
42
- logger.debug("Error reading .env file", { path: envPath, error: String(err) });
43
- }
44
- }
45
- }
46
-
47
- return null;
16
+ export function findApiKey(): string | null {
17
+ return $env.EXA_API_KEY;
48
18
  }
49
19
 
50
20
  /** Fetch available tools from Exa MCP */
@@ -249,17 +219,14 @@ export async function fetchMCPToolSchema(
249
219
  export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
250
220
  public readonly name: string;
251
221
  public readonly label: string;
252
- public readonly description: string;
253
- public readonly parameters: TSchema;
254
-
255
- private readonly config: MCPToolWrapperConfig;
256
222
 
257
- constructor(config: MCPToolWrapperConfig, schema: TSchema, description: string) {
258
- this.config = config;
223
+ constructor(
224
+ private readonly config: MCPToolWrapperConfig,
225
+ public readonly parameters: TSchema,
226
+ public readonly description: string,
227
+ ) {
259
228
  this.name = config.name;
260
229
  this.label = config.label;
261
- this.description = description;
262
- this.parameters = schema;
263
230
  }
264
231
 
265
232
  async execute(
@@ -102,7 +102,7 @@ interface SessionData {
102
102
  /** Generate HTML from bundled template with runtime substitutions. */
103
103
  async function generateHtml(sessionData: SessionData, themeName?: string): Promise<string> {
104
104
  const themeVars = await generateThemeVars(themeName);
105
- const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
105
+ const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64();
106
106
 
107
107
  return TEMPLATE.replace("<theme-vars/>", `<style>:root { ${themeVars} }</style>`).replace(
108
108
  "{{SESSION_DATA}}",
@@ -103,17 +103,14 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
103
103
  readonly logger = logger;
104
104
  readonly typebox = TypeBox;
105
105
  readonly pi = piCodingAgent;
106
- readonly events: EventBus;
107
106
  readonly flagValues = new Map<string, boolean | string>();
108
107
 
109
108
  constructor(
110
- private extension: Extension,
111
- private runtime: IExtensionRuntime,
112
- private cwd: string,
113
- eventBus: EventBus,
114
- ) {
115
- this.events = eventBus;
116
- }
109
+ private readonly extension: Extension,
110
+ private readonly runtime: IExtensionRuntime,
111
+ private readonly cwd: string,
112
+ public readonly events: EventBus,
113
+ ) {}
117
114
 
118
115
  on<F extends HandlerFn>(event: string, handler: F): void {
119
116
  const list = this.extension.handlers.get(event) ?? [];
@@ -214,8 +211,8 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
214
211
  return this.runtime.getThinkingLevel();
215
212
  }
216
213
 
217
- setThinkingLevel(level: ThinkingLevel): void {
218
- this.runtime.setThinkingLevel(level);
214
+ setThinkingLevel(level: ThinkingLevel, persist?: boolean): void {
215
+ this.runtime.setThinkingLevel(level, persist);
219
216
  }
220
217
  }
221
218
 
@@ -105,12 +105,7 @@ const noOpUIContext: ExtensionUIContext = {
105
105
  };
106
106
 
107
107
  export class ExtensionRunner {
108
- private extensions: Extension[];
109
- private runtime: ExtensionRuntime;
110
108
  private uiContext: ExtensionUIContext;
111
- private cwd: string;
112
- private sessionManager: SessionManager;
113
- private modelRegistry: ModelRegistry;
114
109
  private errorListeners: Set<ExtensionErrorListener> = new Set();
115
110
  private getModel: () => Model | undefined = () => undefined;
116
111
  private isIdleFn: () => boolean = () => true;
@@ -125,18 +120,13 @@ export class ExtensionRunner {
125
120
  private shutdownHandler: ShutdownHandler = () => {};
126
121
 
127
122
  constructor(
128
- extensions: Extension[],
129
- runtime: ExtensionRuntime,
130
- cwd: string,
131
- sessionManager: SessionManager,
132
- modelRegistry: ModelRegistry,
123
+ private readonly extensions: Extension[],
124
+ private readonly runtime: ExtensionRuntime,
125
+ private readonly cwd: string,
126
+ private readonly sessionManager: SessionManager,
127
+ private readonly modelRegistry: ModelRegistry,
133
128
  ) {
134
- this.extensions = extensions;
135
- this.runtime = runtime;
136
129
  this.uiContext = noOpUIContext;
137
- this.cwd = cwd;
138
- this.sessionManager = sessionManager;
139
- this.modelRegistry = modelRegistry;
140
130
  }
141
131
 
142
132
  initialize(
@@ -839,7 +839,7 @@ export type SetModelHandler = (model: Model) => Promise<boolean>;
839
839
 
840
840
  export type GetThinkingLevelHandler = () => ThinkingLevel;
841
841
 
842
- export type SetThinkingLevelHandler = (level: ThinkingLevel) => void;
842
+ export type SetThinkingLevelHandler = (level: ThinkingLevel, persist?: boolean) => void;
843
843
 
844
844
  /** Shared state created by loader, used during registration and runtime. */
845
845
  export interface ExtensionRuntimeState {
@@ -62,12 +62,8 @@ const noOpUIContext: HookUIContext = {
62
62
  * HookRunner executes hooks and manages event emission.
63
63
  */
64
64
  export class HookRunner {
65
- private hooks: LoadedHook[];
66
65
  private uiContext: HookUIContext;
67
66
  private hasUI: boolean;
68
- private cwd: string;
69
- private sessionManager: SessionManager;
70
- private modelRegistry: ModelRegistry;
71
67
  private errorListeners: Set<HookErrorListener> = new Set();
72
68
  private getModel: () => Model | undefined = () => undefined;
73
69
  private isIdleFn: () => boolean = () => true;
@@ -78,13 +74,14 @@ export class HookRunner {
78
74
  private branchHandler: BranchHandler = async () => ({ cancelled: false });
79
75
  private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
80
76
 
81
- constructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) {
82
- this.hooks = hooks;
77
+ constructor(
78
+ private readonly hooks: LoadedHook[],
79
+ private readonly cwd: string,
80
+ private readonly sessionManager: SessionManager,
81
+ private readonly modelRegistry: ModelRegistry,
82
+ ) {
83
83
  this.uiContext = noOpUIContext;
84
84
  this.hasUI = false;
85
- this.cwd = cwd;
86
- this.sessionManager = sessionManager;
87
- this.modelRegistry = modelRegistry;
88
85
  }
89
86
 
90
87
  /**
package/src/index.ts CHANGED
@@ -167,7 +167,6 @@ export {
167
167
  discoverCustomTSCommands,
168
168
  discoverExtensions,
169
169
  discoverMCPServers,
170
- discoverModels,
171
170
  discoverPromptTemplates,
172
171
  discoverSkills,
173
172
  EditTool,
package/src/ipy/kernel.ts CHANGED
@@ -16,11 +16,11 @@ const PRELUDE_INTROSPECTION_SNIPPET = "import json\nprint(json.dumps(__omp_prelu
16
16
  const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
17
17
 
18
18
  class SharedGatewayCreateError extends Error {
19
- readonly status: number;
20
-
21
- constructor(status: number, message: string) {
19
+ constructor(
20
+ readonly status: number,
21
+ message: string,
22
+ ) {
22
23
  super(message);
23
- this.status = status;
24
24
  }
25
25
  }
26
26
 
@@ -265,12 +265,6 @@ export function serializeWebSocketMessage(msg: JupyterMessage): ArrayBuffer {
265
265
  }
266
266
 
267
267
  export class PythonKernel {
268
- readonly id: string;
269
- readonly kernelId: string;
270
- readonly gatewayUrl: string;
271
- readonly sessionId: string;
272
- readonly username: string;
273
- readonly isSharedGateway: boolean;
274
268
  readonly #authToken?: string;
275
269
 
276
270
  #ws: WebSocket | null = null;
@@ -281,20 +275,14 @@ export class PythonKernel {
281
275
  #pendingExecutions = new Map<string, (reason: string) => void>();
282
276
 
283
277
  private constructor(
284
- id: string,
285
- kernelId: string,
286
- gatewayUrl: string,
287
- sessionId: string,
288
- username: string,
289
- isSharedGateway: boolean,
278
+ readonly id: string,
279
+ readonly kernelId: string,
280
+ readonly gatewayUrl: string,
281
+ readonly sessionId: string,
282
+ readonly username: string,
283
+ readonly isSharedGateway: boolean,
290
284
  authToken?: string,
291
285
  ) {
292
- this.id = id;
293
- this.kernelId = kernelId;
294
- this.gatewayUrl = gatewayUrl;
295
- this.sessionId = sessionId;
296
- this.username = username;
297
- this.isSharedGateway = isSharedGateway;
298
286
  this.#authToken = authToken;
299
287
  }
300
288
 
@@ -107,18 +107,15 @@ async function runBiome(
107
107
  * Parses Biome's --reporter=json output into LSP Diagnostic format.
108
108
  */
109
109
  export class BiomeClient implements LinterClient {
110
- private config: ServerConfig;
111
- private cwd: string;
112
-
113
110
  /** Factory method for creating BiomeClient instances */
114
111
  static create(config: ServerConfig, cwd: string): LinterClient {
115
112
  return new BiomeClient(config, cwd);
116
113
  }
117
114
 
118
- constructor(config: ServerConfig, cwd: string) {
119
- this.config = config;
120
- this.cwd = cwd;
121
- }
115
+ constructor(
116
+ private readonly config: ServerConfig,
117
+ private readonly cwd: string,
118
+ ) {}
122
119
 
123
120
  async format(filePath: string, content: string): Promise<string> {
124
121
  // Write content to file first
@@ -21,8 +21,6 @@ const DEFAULT_FORMAT_OPTIONS = {
21
21
  * Wraps the existing LSP client infrastructure.
22
22
  */
23
23
  export class LspLinterClient implements LinterClient {
24
- private config: ServerConfig;
25
- private cwd: string;
26
24
  private client: LspClient | null = null;
27
25
 
28
26
  /** Factory method for creating LspLinterClient instances */
@@ -30,10 +28,10 @@ export class LspLinterClient implements LinterClient {
30
28
  return new LspLinterClient(config, cwd);
31
29
  }
32
30
 
33
- constructor(config: ServerConfig, cwd: string) {
34
- this.config = config;
35
- this.cwd = cwd;
36
- }
31
+ constructor(
32
+ private readonly config: ServerConfig,
33
+ private readonly cwd: string,
34
+ ) {}
37
35
 
38
36
  private async getClient(): Promise<LspClient> {
39
37
  if (!this.client) {