@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.
- package/CHANGELOG.md +199 -49
- package/README.md +1 -1
- package/docs/config-usage.md +3 -4
- package/docs/sdk.md +6 -5
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +1 -1
- package/package.json +19 -11
- package/src/cli/args.ts +11 -94
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/oclif-help.ts +26 -0
- package/src/cli/web-search-cli.ts +148 -0
- package/src/cli.ts +8 -2
- package/src/commands/commit.ts +36 -0
- package/src/commands/config.ts +51 -0
- package/src/commands/grep.ts +41 -0
- package/src/commands/index/index.ts +136 -0
- package/src/commands/jupyter.ts +32 -0
- package/src/commands/plugin.ts +70 -0
- package/src/commands/setup.ts +39 -0
- package/src/commands/shell.ts +29 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/update.ts +21 -0
- package/src/commands/web-search.ts +50 -0
- package/src/commit/agentic/index.ts +3 -2
- package/src/commit/agentic/tools/analyze-file.ts +1 -3
- package/src/commit/git/errors.ts +4 -6
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +1 -3
- package/src/config/model-registry.ts +89 -162
- package/src/config/settings-schema.ts +10 -0
- package/src/config.ts +202 -132
- package/src/exa/mcp-client.ts +8 -41
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -10
- package/src/extensibility/extensions/runner.ts +5 -15
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/hooks/runner.ts +6 -9
- package/src/index.ts +0 -1
- package/src/ipy/kernel.ts +10 -22
- package/src/lsp/clients/biome-client.ts +4 -7
- package/src/lsp/clients/lsp-linter-client.ts +4 -6
- package/src/lsp/index.ts +5 -4
- package/src/lsp/utils.ts +18 -0
- package/src/main.ts +86 -181
- package/src/mcp/json-rpc.ts +2 -2
- package/src/mcp/transports/http.ts +12 -49
- package/src/modes/components/armin.ts +1 -3
- package/src/modes/components/assistant-message.ts +4 -4
- package/src/modes/components/bash-execution.ts +5 -3
- package/src/modes/components/branch-summary-message.ts +1 -3
- package/src/modes/components/compaction-summary-message.ts +1 -3
- package/src/modes/components/custom-message.ts +4 -5
- package/src/modes/components/extensions/extension-dashboard.ts +10 -16
- package/src/modes/components/extensions/extension-list.ts +5 -5
- package/src/modes/components/footer.ts +1 -4
- package/src/modes/components/hook-editor.ts +7 -32
- package/src/modes/components/hook-message.ts +4 -5
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +16 -20
- package/src/modes/components/python-execution.ts +5 -5
- package/src/modes/components/session-selector.ts +6 -7
- package/src/modes/components/settings-defs.ts +49 -40
- package/src/modes/components/settings-selector.ts +8 -17
- package/src/modes/components/skill-message.ts +1 -3
- package/src/modes/components/status-line-segment-editor.ts +1 -3
- package/src/modes/components/status-line.ts +1 -3
- package/src/modes/components/todo-reminder.ts +5 -7
- package/src/modes/components/tree-selector.ts +10 -12
- package/src/modes/components/ttsr-notification.ts +1 -3
- package/src/modes/components/user-message-selector.ts +2 -4
- package/src/modes/components/welcome.ts +6 -18
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +7 -34
- package/src/modes/controllers/selector-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +27 -1
- package/src/modes/rpc/rpc-client.ts +2 -5
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/theme/theme.ts +2 -6
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -1
- package/src/patch/index.ts +1 -4
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/frontmatter.md +2 -1
- package/src/prompts/agents/init.md +1 -0
- package/src/prompts/agents/plan.md +1 -0
- package/src/prompts/agents/reviewer.md +1 -0
- package/src/prompts/system/subagent-submit-reminder.md +2 -0
- package/src/prompts/system/subagent-system-prompt.md +2 -0
- package/src/prompts/system/subagent-user-prompt.md +8 -0
- package/src/prompts/system/system-prompt.md +5 -3
- package/src/prompts/system/web-search.md +6 -4
- package/src/prompts/tools/task.md +216 -163
- package/src/sdk.ts +11 -110
- package/src/session/agent-session.ts +117 -83
- package/src/session/auth-storage.ts +10 -51
- package/src/session/messages.ts +17 -3
- package/src/session/session-manager.ts +30 -30
- package/src/session/streaming-output.ts +1 -1
- package/src/ssh/ssh-executor.ts +6 -3
- package/src/task/agents.ts +2 -0
- package/src/task/discovery.ts +1 -1
- package/src/task/executor.ts +5 -10
- package/src/task/index.ts +43 -23
- package/src/task/render.ts +67 -64
- package/src/task/template.ts +17 -34
- package/src/task/types.ts +49 -22
- package/src/tools/ask.ts +1 -3
- package/src/tools/bash.ts +1 -4
- package/src/tools/browser.ts +5 -7
- package/src/tools/exit-plan-mode.ts +1 -4
- package/src/tools/fetch.ts +1 -3
- package/src/tools/find.ts +4 -3
- package/src/tools/gemini-image.ts +24 -55
- package/src/tools/grep.ts +4 -4
- package/src/tools/index.ts +12 -14
- package/src/tools/notebook.ts +1 -5
- package/src/tools/python.ts +4 -3
- package/src/tools/read.ts +2 -4
- package/src/tools/render-utils.ts +23 -0
- package/src/tools/ssh.ts +8 -12
- package/src/tools/todo-write.ts +1 -4
- package/src/tools/tool-errors.ts +1 -4
- package/src/tools/write.ts +1 -3
- package/src/utils/external-editor.ts +59 -0
- package/src/utils/file-mentions.ts +39 -1
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/web/search/auth.ts +3 -33
- package/src/web/search/index.ts +73 -139
- package/src/web/search/provider.ts +58 -0
- package/src/web/search/providers/anthropic.ts +53 -14
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/codex.ts +38 -16
- package/src/web/search/providers/exa.ts +30 -6
- package/src/web/search/providers/gemini.ts +56 -20
- package/src/web/search/providers/jina.ts +28 -5
- package/src/web/search/providers/perplexity.ts +103 -36
- package/src/web/search/render.ts +84 -74
- package/src/web/search/types.ts +285 -59
- package/src/migrations.ts +0 -175
- 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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
75
|
-
export function
|
|
76
|
-
return path.join(getAgentDir(), "
|
|
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
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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);
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -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
|
|
23
|
-
|
|
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(
|
|
258
|
-
|
|
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(
|
package/src/export/html/index.ts
CHANGED
|
@@ -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)).
|
|
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
|
-
|
|
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(
|
|
82
|
-
|
|
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
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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(
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
34
|
-
|
|
35
|
-
|
|
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) {
|