@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2
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 +79 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +10 -10
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +44 -3
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +82 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +11 -12
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +327 -95
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +93 -8
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +41 -88
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +116 -0
- package/src/modes/theme/defaults/dark-poimandres.json +3 -0
- package/src/modes/theme/defaults/light-poimandres.json +3 -0
- package/src/modes/theme/theme.ts +24 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +101 -117
- package/src/prompts/tools/read.md +55 -36
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +19 -8
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +66 -19
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +12 -2
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +44 -10
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +202 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +548 -237
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +64 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +40 -95
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/config.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
CONFIG_DIR_NAME,
|
|
6
|
-
getAgentDir,
|
|
7
|
-
getConfigAgentDirName,
|
|
8
|
-
getProjectDir,
|
|
9
|
-
isEnoent,
|
|
10
|
-
logger,
|
|
11
|
-
} from "@oh-my-pi/pi-utils";
|
|
12
|
-
import type { TSchema } from "@sinclair/typebox";
|
|
13
|
-
import { Value } from "@sinclair/typebox/value";
|
|
14
|
-
import type { ErrorObject } from "ajv";
|
|
15
|
-
import { JSONC, YAML } from "bun";
|
|
4
|
+
import { CONFIG_DIR_NAME, getConfigAgentDirName, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
16
5
|
import { expandTilde } from "./tools/path-utils";
|
|
17
6
|
|
|
7
|
+
export * from "./config/config-file";
|
|
8
|
+
|
|
18
9
|
const priorityList = [
|
|
19
10
|
{ dir: CONFIG_DIR_NAME, globalAgentDir: getConfigAgentDirName },
|
|
20
11
|
{ dir: ".claude" },
|
|
@@ -53,213 +44,6 @@ export function getChangelogPath(): string {
|
|
|
53
44
|
return path.resolve(path.join(getPackageDir(), "CHANGELOG.md"));
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
// =============================================================================
|
|
57
|
-
// User Config Paths (~/.omp/agent/*)
|
|
58
|
-
// =============================================================================
|
|
59
|
-
|
|
60
|
-
function migrateJsonToYml(jsonPath: string, ymlPath: string) {
|
|
61
|
-
try {
|
|
62
|
-
if (fs.existsSync(ymlPath)) return;
|
|
63
|
-
if (!fs.existsSync(jsonPath)) return;
|
|
64
|
-
|
|
65
|
-
const content = fs.readFileSync(jsonPath, "utf-8");
|
|
66
|
-
const parsed = JSON.parse(content);
|
|
67
|
-
if (!parsed) {
|
|
68
|
-
logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
fs.writeFileSync(ymlPath, YAML.stringify(parsed, null, 2));
|
|
72
|
-
} catch (error) {
|
|
73
|
-
logger.warn("migrateJsonToYml: migration failed", { error: String(error) });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface IConfigFile<T> {
|
|
78
|
-
readonly id: string;
|
|
79
|
-
readonly schema: TSchema;
|
|
80
|
-
path?(): string;
|
|
81
|
-
load(): T | null;
|
|
82
|
-
invalidate?(): void;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export class ConfigError extends Error {
|
|
86
|
-
readonly #message: string;
|
|
87
|
-
constructor(
|
|
88
|
-
public readonly id: string,
|
|
89
|
-
public readonly schemaErrors: ErrorObject[] | null | undefined,
|
|
90
|
-
public readonly other?: { err: unknown; stage: string },
|
|
91
|
-
) {
|
|
92
|
-
let messages: string[] | undefined;
|
|
93
|
-
let cause: any | undefined;
|
|
94
|
-
let klass: string;
|
|
95
|
-
|
|
96
|
-
if (schemaErrors) {
|
|
97
|
-
klass = "Schema";
|
|
98
|
-
messages = schemaErrors.map(e => `${e.instancePath || "root"}: ${e.message}`);
|
|
99
|
-
} else if (other) {
|
|
100
|
-
klass = other.stage;
|
|
101
|
-
if (other.err instanceof Error) {
|
|
102
|
-
messages = [other.err.message];
|
|
103
|
-
cause = other.err;
|
|
104
|
-
} else {
|
|
105
|
-
messages = [String(other.err)];
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
klass = "Unknown";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const title = `Failed to load config file ${id}, ${klass} error:`;
|
|
112
|
-
let message: string;
|
|
113
|
-
switch (messages?.length ?? 0) {
|
|
114
|
-
case 0:
|
|
115
|
-
message = title.slice(0, -1);
|
|
116
|
-
break;
|
|
117
|
-
case 1:
|
|
118
|
-
message = `${title} ${messages![0]}`;
|
|
119
|
-
break;
|
|
120
|
-
default:
|
|
121
|
-
message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
super(message, { cause });
|
|
126
|
-
this.name = "LoadError";
|
|
127
|
-
this.#message = message;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
get message(): string {
|
|
131
|
-
return this.#message;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
toString(): string {
|
|
135
|
-
return this.message;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export type LoadStatus = "ok" | "error" | "not-found";
|
|
140
|
-
|
|
141
|
-
export type LoadResult<T> =
|
|
142
|
-
| { value?: null; error: ConfigError; status: "error" }
|
|
143
|
-
| { value: T; error?: undefined; status: "ok" }
|
|
144
|
-
| { value?: null; error?: unknown; status: "not-found" };
|
|
145
|
-
|
|
146
|
-
export class ConfigFile<T> implements IConfigFile<T> {
|
|
147
|
-
readonly #basePath: string;
|
|
148
|
-
#cache?: LoadResult<T>;
|
|
149
|
-
#auxValidate?: (value: T) => void;
|
|
150
|
-
|
|
151
|
-
constructor(
|
|
152
|
-
readonly id: string,
|
|
153
|
-
readonly schema: TSchema,
|
|
154
|
-
configPath: string = path.join(getAgentDir(), `${id}.yml`),
|
|
155
|
-
) {
|
|
156
|
-
this.#basePath = configPath;
|
|
157
|
-
if (configPath.endsWith(".yml")) {
|
|
158
|
-
const jsonPath = `${configPath.slice(0, -4)}.json`;
|
|
159
|
-
migrateJsonToYml(jsonPath, configPath);
|
|
160
|
-
} else if (configPath.endsWith(".yaml")) {
|
|
161
|
-
const jsonPath = `${configPath.slice(0, -5)}.json`;
|
|
162
|
-
migrateJsonToYml(jsonPath, configPath);
|
|
163
|
-
} else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
|
|
164
|
-
// JSON configs are still supported without migration.
|
|
165
|
-
} else {
|
|
166
|
-
throw new Error(`Invalid config file path: ${configPath}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
relocate(path?: string): ConfigFile<T> {
|
|
171
|
-
if (!path || path === this.#basePath) return this;
|
|
172
|
-
const result = new ConfigFile<T>(this.id, this.schema, path);
|
|
173
|
-
result.#auxValidate = this.#auxValidate;
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
getMtimeMs(): number | null {
|
|
178
|
-
try {
|
|
179
|
-
return fs.statSync(this.path()).mtimeMs;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
if (isEnoent(err)) return null;
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
withValidation(name: string, validate: (value: T) => void): this {
|
|
187
|
-
const prev = this.#auxValidate;
|
|
188
|
-
this.#auxValidate = (value: T) => {
|
|
189
|
-
prev?.(value);
|
|
190
|
-
try {
|
|
191
|
-
validate(value);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
throw new ConfigError(this.id, undefined, { err: error, stage: `Validate(${name})` });
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
createDefault() {
|
|
200
|
-
return Value.Default(this.schema, [], undefined) as T;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
#storeCache(result: LoadResult<T>): LoadResult<T> {
|
|
204
|
-
this.#cache = result;
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
tryLoad(): LoadResult<T> {
|
|
209
|
-
if (this.#cache) return this.#cache;
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const content = fs.readFileSync(this.path(), "utf-8").trim();
|
|
213
|
-
|
|
214
|
-
let parsed: unknown;
|
|
215
|
-
if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
|
|
216
|
-
parsed = JSONC.parse(content);
|
|
217
|
-
} else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
|
|
218
|
-
parsed = YAML.parse(content);
|
|
219
|
-
} else {
|
|
220
|
-
throw new Error(`Invalid config file path: ${this.#basePath}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!Value.Check(this.schema, parsed)) {
|
|
224
|
-
const schemaErrors: ErrorObject[] = [];
|
|
225
|
-
for (const err of Value.Errors(this.schema, parsed)) {
|
|
226
|
-
schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
|
|
227
|
-
if (schemaErrors.length >= 50) break;
|
|
228
|
-
}
|
|
229
|
-
const error = new ConfigError(this.id, schemaErrors);
|
|
230
|
-
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
231
|
-
return this.#storeCache({ error, status: "error" });
|
|
232
|
-
}
|
|
233
|
-
return this.#storeCache({ value: parsed as T, status: "ok" });
|
|
234
|
-
} catch (error) {
|
|
235
|
-
if (isEnoent(error)) {
|
|
236
|
-
return this.#storeCache({ status: "not-found" });
|
|
237
|
-
}
|
|
238
|
-
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
239
|
-
return this.#storeCache({
|
|
240
|
-
error: new ConfigError(this.id, undefined, { err: error, stage: "Unexpected" }),
|
|
241
|
-
status: "error",
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
load(): T | null {
|
|
247
|
-
return this.tryLoad().value ?? null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
loadOrDefault(): T {
|
|
251
|
-
return this.tryLoad().value ?? this.createDefault();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
path(): string {
|
|
255
|
-
return this.#basePath;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
invalidate() {
|
|
259
|
-
this.#cache = undefined;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
47
|
// =============================================================================
|
|
264
48
|
// Multi-Config Directory Helpers
|
|
265
49
|
// =============================================================================
|
|
@@ -31,6 +31,7 @@ const PRIORITY = 70; // Below claude.ts (80) so user .claude/ overrides win
|
|
|
31
31
|
interface ClaudePluginManifest {
|
|
32
32
|
skills?: string;
|
|
33
33
|
"slash-commands"?: string;
|
|
34
|
+
commands?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
interface ResolvedPluginDir {
|
|
@@ -59,24 +60,35 @@ function isWithinPluginRoot(rootPath: string, targetPath: string): boolean {
|
|
|
59
60
|
|
|
60
61
|
async function resolvePluginDir(
|
|
61
62
|
root: ClaudePluginRoot,
|
|
62
|
-
|
|
63
|
+
manifestKeys: ReadonlyArray<keyof ClaudePluginManifest>,
|
|
63
64
|
fallback: string,
|
|
64
65
|
): Promise<ResolvedPluginDir> {
|
|
65
66
|
const manifest = await readPluginManifest(root);
|
|
66
67
|
const fallbackDir = path.join(root.path, fallback);
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
let configured: string | undefined;
|
|
70
|
+
let matchedKey: keyof ClaudePluginManifest | undefined;
|
|
71
|
+
for (const key of manifestKeys) {
|
|
72
|
+
const val = manifest?.[key];
|
|
73
|
+
if (typeof val === "string" && val.trim()) {
|
|
74
|
+
configured = val.trim();
|
|
75
|
+
matchedKey = key;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (configured === undefined) {
|
|
69
81
|
return { dir: fallbackDir };
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
const resolved = path.resolve(root.path, configured
|
|
84
|
+
const resolved = path.resolve(root.path, configured);
|
|
73
85
|
if (isWithinPluginRoot(root.path, resolved)) {
|
|
74
86
|
return { dir: resolved };
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
return {
|
|
78
90
|
dir: fallbackDir,
|
|
79
|
-
warning: `[claude-plugins] Ignoring ${String(
|
|
91
|
+
warning: `[claude-plugins] Ignoring ${String(matchedKey)} path outside plugin root for ${root.id}: ${configured}`,
|
|
80
92
|
};
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -93,7 +105,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
93
105
|
|
|
94
106
|
const results = await Promise.all(
|
|
95
107
|
roots.map(async root => {
|
|
96
|
-
const { dir: skillsDir, warning } = await resolvePluginDir(root, "skills", "skills");
|
|
108
|
+
const { dir: skillsDir, warning } = await resolvePluginDir(root, ["skills"], "skills");
|
|
97
109
|
const result = await scanSkillsFromDir(ctx, {
|
|
98
110
|
dir: skillsDir,
|
|
99
111
|
providerId: PROVIDER_ID,
|
|
@@ -128,7 +140,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
128
140
|
|
|
129
141
|
const results = await Promise.all(
|
|
130
142
|
roots.map(async root => {
|
|
131
|
-
const { dir: commandsDir, warning } = await resolvePluginDir(root, "slash-commands", "commands");
|
|
143
|
+
const { dir: commandsDir, warning } = await resolvePluginDir(root, ["commands", "slash-commands"], "commands");
|
|
132
144
|
const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, root.scope, {
|
|
133
145
|
extensions: ["md"],
|
|
134
146
|
transform: (name, content, filePath, source) => {
|
package/src/edit/renderer.ts
CHANGED
|
@@ -340,7 +340,13 @@ function normalizeHashlineInputPreviewPath(rawPath: string): string {
|
|
|
340
340
|
|
|
341
341
|
function parseHashlineInputPreviewHeader(line: string): string | null {
|
|
342
342
|
if (!line.startsWith(HL_INPUT_HEADER_PREFIX)) return null;
|
|
343
|
-
|
|
343
|
+
// The real parser (`parseHashlineHeaderLine` in `hashline/input.ts`) strips
|
|
344
|
+
// every leading "@" before resolving the path so canonical "@@ PATH" headers
|
|
345
|
+
// (and stray "@ PATH" / "@@@ PATH" runs) all route to the same file. Mirror
|
|
346
|
+
// that here so the renderer doesn't surface a literal "@ " in the title.
|
|
347
|
+
let prefixEnd = 0;
|
|
348
|
+
while (prefixEnd < line.length && line[prefixEnd] === HL_INPUT_HEADER_PREFIX) prefixEnd++;
|
|
349
|
+
const body = line.slice(prefixEnd).trim();
|
|
344
350
|
const previewPath = normalizeHashlineInputPreviewPath(body);
|
|
345
351
|
return previewPath.length > 0 ? previewPath : null;
|
|
346
352
|
}
|
package/src/eval/js/executor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_MAX_BYTES, OutputSink } from "../../session/streaming-output";
|
|
2
2
|
import type { ToolSession } from "../../tools";
|
|
3
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
|
|
3
4
|
import { executeInVmContext, type JsDisplayOutput } from "./context-manager";
|
|
4
5
|
|
|
5
6
|
export interface JsExecutorOptions {
|
|
@@ -49,6 +50,8 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
49
50
|
artifactPath: options.artifactPath,
|
|
50
51
|
artifactId: options.artifactId,
|
|
51
52
|
spillThreshold: DEFAULT_MAX_BYTES,
|
|
53
|
+
headBytes: resolveOutputSinkHeadBytes(options.session.settings),
|
|
54
|
+
maxColumns: resolveOutputMaxColumns(options.session.settings),
|
|
52
55
|
onChunk: chunk => options.onChunk?.(chunk),
|
|
53
56
|
});
|
|
54
57
|
const timeoutMs = getExecutionTimeoutMs(options);
|
|
@@ -180,7 +180,7 @@ export function rewriteImports(code: string): string {
|
|
|
180
180
|
* Nested declarations (inside functions, blocks, classes) are left alone \u2014 they're
|
|
181
181
|
* scoped to their enclosing function/block regardless of `var` vs `let`/`const`.
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
function demoteTopLevelLexicals(code: string): string {
|
|
184
184
|
if (!/\b(?:const|let|class)\b/.test(code)) return code;
|
|
185
185
|
|
|
186
186
|
const ast = parseProgram(code);
|
|
@@ -248,7 +248,7 @@ function returnFinalExpression(code: string): { source: string; returned: boolea
|
|
|
248
248
|
* common case avoids an extra transpile pass. We detect "looks like TS" with a cheap regex
|
|
249
249
|
* before invoking the transpiler.
|
|
250
250
|
*/
|
|
251
|
-
|
|
251
|
+
function stripTypeScript(code: string): string {
|
|
252
252
|
if (!LOOKS_LIKE_TS.test(code)) return code;
|
|
253
253
|
try {
|
|
254
254
|
return new Bun.Transpiler({ loader: "ts" }).transformSync(code);
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { Settings } from "../../config/settings";
|
|
2
3
|
import { OutputSink } from "../../session/streaming-output";
|
|
3
4
|
import type { ToolSession } from "../../tools";
|
|
5
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
|
|
4
6
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
5
7
|
import type { KernelDisplayOutput } from "./display";
|
|
6
8
|
import {
|
|
@@ -815,10 +817,13 @@ async function executeWithKernel(
|
|
|
815
817
|
code: string,
|
|
816
818
|
options: PythonExecutorOptions | undefined,
|
|
817
819
|
): Promise<PythonResult> {
|
|
820
|
+
const settings = await Settings.init();
|
|
818
821
|
const sink = new OutputSink({
|
|
819
822
|
onChunk: options?.onChunk,
|
|
820
823
|
artifactPath: options?.artifactPath,
|
|
821
824
|
artifactId: options?.artifactId,
|
|
825
|
+
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
826
|
+
maxColumns: resolveOutputMaxColumns(settings),
|
|
822
827
|
});
|
|
823
828
|
const displayOutputs: KernelDisplayOutput[] = [];
|
|
824
829
|
const deadlineMs = getExecutionDeadlineMs(options);
|
package/src/eval/py/runner.py
CHANGED
|
@@ -25,9 +25,11 @@ when installed.
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
+
import asyncio
|
|
28
29
|
import ast
|
|
29
30
|
import base64
|
|
30
31
|
import builtins
|
|
32
|
+
import inspect
|
|
31
33
|
import io
|
|
32
34
|
import json
|
|
33
35
|
import os
|
|
@@ -120,6 +122,7 @@ class _RunnerState:
|
|
|
120
122
|
"__builtins__": builtins,
|
|
121
123
|
}
|
|
122
124
|
self.last_install_marker: int = 0
|
|
125
|
+
self.loop: asyncio.AbstractEventLoop | None = None
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
_STATE = _RunnerState()
|
|
@@ -688,13 +691,41 @@ _install_builtins(_STATE.user_ns)
|
|
|
688
691
|
# ---------------------------------------------------------------------------
|
|
689
692
|
|
|
690
693
|
|
|
694
|
+
_TLA_FLAG = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x2000)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _get_event_loop() -> asyncio.AbstractEventLoop:
|
|
698
|
+
loop = _STATE.loop
|
|
699
|
+
if loop is None or loop.is_closed():
|
|
700
|
+
loop = asyncio.new_event_loop()
|
|
701
|
+
asyncio.set_event_loop(loop)
|
|
702
|
+
_STATE.loop = loop
|
|
703
|
+
return loop
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _run_compiled(code, ns: dict, *, want_value: bool) -> Any:
|
|
707
|
+
"""Execute a code object, awaiting it if compiled as a coroutine.
|
|
708
|
+
|
|
709
|
+
``want_value`` is True for the trailing expression — we return ``eval``'s
|
|
710
|
+
result (or the awaited coroutine's value). For statement blocks the
|
|
711
|
+
return is always ``None``.
|
|
712
|
+
"""
|
|
713
|
+
if code.co_flags & inspect.CO_COROUTINE:
|
|
714
|
+
coro = eval(code, ns)
|
|
715
|
+
result = _get_event_loop().run_until_complete(coro)
|
|
716
|
+
return result if want_value else None
|
|
717
|
+
if want_value:
|
|
718
|
+
return eval(code, ns)
|
|
719
|
+
exec(code, ns)
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
|
|
691
723
|
def _exec_source(source: str, ns: dict) -> None:
|
|
692
724
|
"""Compile + execute ``source``; if the last node is an expression, route
|
|
693
|
-
its value through ``__omp_display`` so dataframes/figures render rich.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
raise
|
|
725
|
+
its value through ``__omp_display`` so dataframes/figures render rich.
|
|
726
|
+
Top-level ``await`` / ``async for`` / ``async with`` is permitted; the
|
|
727
|
+
cell is driven through the runner's persistent event loop."""
|
|
728
|
+
module = ast.parse(source, mode="exec")
|
|
698
729
|
|
|
699
730
|
if not module.body:
|
|
700
731
|
return
|
|
@@ -704,16 +735,16 @@ def _exec_source(source: str, ns: dict) -> None:
|
|
|
704
735
|
body_module = ast.Module(body=module.body[:-1], type_ignores=[])
|
|
705
736
|
expr_module = ast.Expression(body=last.value)
|
|
706
737
|
ast.copy_location(expr_module, last)
|
|
707
|
-
body_code = compile(body_module, "<cell>", "exec")
|
|
708
|
-
expr_code = compile(expr_module, "<cell>", "eval")
|
|
709
|
-
|
|
710
|
-
value =
|
|
738
|
+
body_code = compile(body_module, "<cell>", "exec", flags=_TLA_FLAG)
|
|
739
|
+
expr_code = compile(expr_module, "<cell>", "eval", flags=_TLA_FLAG)
|
|
740
|
+
_run_compiled(body_code, ns, want_value=False)
|
|
741
|
+
value = _run_compiled(expr_code, ns, want_value=True)
|
|
711
742
|
if value is not None:
|
|
712
743
|
__omp_display(value, kind="result")
|
|
713
744
|
return
|
|
714
745
|
|
|
715
|
-
code = compile(module, "<cell>", "exec")
|
|
716
|
-
|
|
746
|
+
code = compile(module, "<cell>", "exec", flags=_TLA_FLAG)
|
|
747
|
+
_run_compiled(code, ns, want_value=False)
|
|
717
748
|
|
|
718
749
|
|
|
719
750
|
# ---------------------------------------------------------------------------
|
package/src/eval/py/runtime.ts
CHANGED
|
@@ -151,6 +151,7 @@ export function filterEnv(env: Record<string, string | undefined>): Record<strin
|
|
|
151
151
|
*/
|
|
152
152
|
export function resolveVenvPath(cwd: string): string | undefined {
|
|
153
153
|
if ($env.VIRTUAL_ENV) return $env.VIRTUAL_ENV;
|
|
154
|
+
if ($env.CONDA_PREFIX) return $env.CONDA_PREFIX;
|
|
154
155
|
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
|
|
155
156
|
for (const candidate of candidates) {
|
|
156
157
|
if (fs.existsSync(candidate)) {
|
package/src/exa/factory.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { TObject, TProperties } from "@sinclair/typebox";
|
|
5
5
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
6
|
-
import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client";
|
|
6
|
+
import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
|
|
7
7
|
import type { ExaRenderDetails } from "./types";
|
|
8
8
|
|
|
9
9
|
/** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
|
|
@@ -44,7 +44,7 @@ export function createExaTool(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return {
|
|
47
|
-
content: [{ type: "text" as const, text:
|
|
47
|
+
content: [{ type: "text" as const, text: formatGenericResponse(response) }],
|
|
48
48
|
details: { raw: response, toolName: name },
|
|
49
49
|
};
|
|
50
50
|
} catch (error) {
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -174,6 +174,79 @@ export function formatSearchResults(data: ExaSearchResponse): string {
|
|
|
174
174
|
|
|
175
175
|
return output.trim();
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Format a non-search MCP response as human-readable text.
|
|
179
|
+
* Handles objects, arrays, primitives, and common MCP response shapes.
|
|
180
|
+
*/
|
|
181
|
+
export function formatGenericResponse(data: unknown): string {
|
|
182
|
+
if (data === null || data === undefined) return "No result.";
|
|
183
|
+
if (typeof data === "string") return data;
|
|
184
|
+
if (typeof data === "number" || typeof data === "boolean") return String(data);
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(data)) {
|
|
187
|
+
if (data.length === 0) return "(empty)";
|
|
188
|
+
const parts: string[] = [];
|
|
189
|
+
for (let i = 0; i < data.length; i++) {
|
|
190
|
+
const item = data[i];
|
|
191
|
+
if (typeof item === "object" && item !== null) {
|
|
192
|
+
const record = item as Record<string, unknown>;
|
|
193
|
+
const title = (record.title ?? record.name ?? record.id ?? `Item ${i + 1}`) as string;
|
|
194
|
+
parts.push(`\n### ${title}`);
|
|
195
|
+
for (const [k, v] of Object.entries(record)) {
|
|
196
|
+
if (["title", "name", "id"].includes(k)) continue;
|
|
197
|
+
parts.push(`- **${k}:** ${formatValue(v)}`);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
parts.push(`- ${formatValue(item)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return parts.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof data === "object") {
|
|
207
|
+
const record = data as Record<string, unknown>;
|
|
208
|
+
if (record.content && Array.isArray(record.content)) {
|
|
209
|
+
// MCP-style content array — extract text blocks
|
|
210
|
+
const texts = record.content
|
|
211
|
+
.filter(
|
|
212
|
+
(c: unknown): c is { type: string; text?: string } =>
|
|
213
|
+
typeof c === "object" && c !== null && (c as Record<string, unknown>)?.type === "text",
|
|
214
|
+
)
|
|
215
|
+
.map(c => c.text ?? "")
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
if (texts.length > 0) return texts.join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
for (const [k, v] of Object.entries(record)) {
|
|
222
|
+
if (k === "content") continue; // handled above
|
|
223
|
+
if (v === null || v === undefined) continue;
|
|
224
|
+
if (typeof v === "object") {
|
|
225
|
+
const formatted = formatGenericResponse(v);
|
|
226
|
+
if (formatted) lines.push(`- **${k}:**\n${indent(formatted, 2)}`);
|
|
227
|
+
} else {
|
|
228
|
+
lines.push(`- **${k}:** ${formatValue(v)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return lines.join("\n") || "(empty)";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return String(data);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatValue(v: unknown): string {
|
|
238
|
+
if (v === null || v === undefined) return "—";
|
|
239
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
240
|
+
return String(v);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function indent(text: string, spaces: number): string {
|
|
244
|
+
const pad = " ".repeat(spaces);
|
|
245
|
+
return text
|
|
246
|
+
.split("\n")
|
|
247
|
+
.map(line => pad + line)
|
|
248
|
+
.join("\n");
|
|
249
|
+
}
|
|
177
250
|
|
|
178
251
|
/** Check if result is a search response */
|
|
179
252
|
export function isSearchResponse(data: unknown): data is ExaSearchResponse {
|
|
@@ -260,7 +333,7 @@ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
|
|
|
260
333
|
}
|
|
261
334
|
|
|
262
335
|
return {
|
|
263
|
-
content: [{ type: "text" as const, text:
|
|
336
|
+
content: [{ type: "text" as const, text: formatGenericResponse(response) }],
|
|
264
337
|
details: { raw: response, toolName: this.config.name },
|
|
265
338
|
};
|
|
266
339
|
} catch (error) {
|
|
@@ -7,6 +7,7 @@ import * as fs from "node:fs/promises";
|
|
|
7
7
|
import { executeShell, type MinimizerOptions, Shell } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
9
9
|
import { OutputSink } from "../session/streaming-output";
|
|
10
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
10
11
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
11
12
|
import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
|
|
12
13
|
|
|
@@ -64,7 +65,8 @@ async function resolveShellCwd(cwd: string | undefined): Promise<string | undefi
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
/** Translate `ShellMinimizerSettings` into native `MinimizerOptions`, or `undefined` when disabled. */
|
|
69
|
+
export function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined {
|
|
68
70
|
if (!group.enabled) return undefined;
|
|
69
71
|
return {
|
|
70
72
|
enabled: true,
|
|
@@ -94,6 +96,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
94
96
|
onChunk: options?.onChunk,
|
|
95
97
|
artifactPath: options?.artifactPath,
|
|
96
98
|
artifactId: options?.artifactId,
|
|
99
|
+
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
100
|
+
maxColumns: resolveOutputMaxColumns(settings),
|
|
97
101
|
// Throttle the streaming preview callback to avoid saturating the
|
|
98
102
|
// event loop when commands produce massive output (e.g. seq 1 50M).
|
|
99
103
|
chunkThrottleMs: options?.onChunk ? 50 : 0,
|