@oh-my-pi/pi-coding-agent 10.6.2 → 11.0.1
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 +44 -0
- package/README.md +80 -79
- package/docs/compaction.md +182 -149
- package/docs/config-usage.md +141 -78
- package/docs/custom-tools.md +45 -16
- package/docs/extension-loading.md +56 -954
- package/docs/extensions.md +192 -51
- package/docs/hooks.md +109 -70
- package/docs/python-repl.md +52 -19
- package/docs/rpc.md +43 -19
- package/docs/sdk.md +270 -211
- package/docs/session-tree-plan.md +60 -417
- package/docs/session.md +104 -39
- package/docs/skills.md +59 -95
- package/docs/theme.md +139 -110
- package/docs/tree.md +42 -33
- package/docs/tui.md +226 -80
- package/package.json +8 -9
- package/src/capability/index.ts +3 -4
- package/src/cli/args.ts +4 -4
- package/src/cli/grep-cli.ts +1 -1
- package/src/commit/agentic/index.ts +4 -3
- package/src/commit/git/index.ts +2 -3
- package/src/commit/map-reduce/index.ts +2 -1
- package/src/config/prompt-templates.ts +2 -0
- package/src/config/settings-schema.ts +30 -7
- package/src/config/settings.ts +0 -14
- package/src/config.ts +2 -2
- package/src/discovery/agents.ts +36 -0
- package/src/discovery/index.ts +1 -0
- package/src/exa/mcp-client.ts +3 -3
- package/src/ipy/executor.ts +5 -7
- package/src/ipy/gateway-coordinator.ts +1 -1
- package/src/ipy/kernel.ts +20 -15
- package/src/ipy/prelude.py +1 -1
- package/src/ipy/runtime.ts +7 -6
- package/src/lsp/lspmux.ts +3 -3
- package/src/main.ts +6 -8
- package/src/mcp/tool-bridge.ts +19 -9
- package/src/modes/components/assistant-message.ts +2 -2
- package/src/modes/components/hook-editor.ts +4 -4
- package/src/modes/components/settings-defs.ts +37 -2
- package/src/modes/components/tool-execution.ts +7 -7
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +4 -7
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -9
- package/src/patch/index.ts +6 -6
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/frontmatter.md +5 -5
- package/src/prompts/agents/plan.md +3 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -3
- package/src/sdk.ts +13 -9
- package/src/session/agent-session.ts +6 -4
- package/src/session/compaction/compaction.ts +3 -3
- package/src/session/session-manager.ts +8 -9
- package/src/ssh/connection-manager.ts +4 -4
- package/src/system-prompt.ts +2 -6
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +14 -35
- package/src/task/omp-command.ts +3 -1
- package/src/task/output-manager.ts +20 -6
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +16 -2
- package/src/task/types.ts +13 -20
- package/src/task/worktree.ts +3 -3
- package/src/tools/ask.ts +3 -8
- package/src/tools/fetch.ts +2 -2
- package/src/tools/gemini-image.ts +5 -6
- package/src/tools/grep.ts +5 -5
- package/src/tools/index.ts +12 -5
- package/src/tools/read.ts +1 -1
- package/src/tools/todo-write.ts +2 -3
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/timings.ts +3 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/utils.ts +2 -3
- package/src/web/scrapers/youtube.ts +2 -3
- package/src/web/search/auth.ts +5 -6
- package/src/web/search/providers/anthropic.ts +3 -2
- package/src/utils/terminal-notify.ts +0 -37
|
@@ -246,7 +246,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
246
246
|
enabledModels: { type: "array", default: [] as string[] },
|
|
247
247
|
disabledProviders: { type: "array", default: [] as string[] },
|
|
248
248
|
disabledExtensions: { type: "array", default: [] as string[] },
|
|
249
|
-
env: { type: "record", default: {} as Record<string, string> },
|
|
250
249
|
modelRoles: { type: "record", default: {} as Record<string, string> },
|
|
251
250
|
|
|
252
251
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -397,6 +396,30 @@ export const SETTINGS_SCHEMA = {
|
|
|
397
396
|
},
|
|
398
397
|
},
|
|
399
398
|
|
|
399
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
400
|
+
// Task tool settings
|
|
401
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
402
|
+
"task.maxConcurrency": {
|
|
403
|
+
type: "number",
|
|
404
|
+
default: 32,
|
|
405
|
+
ui: {
|
|
406
|
+
tab: "tools",
|
|
407
|
+
label: "Task max concurrency",
|
|
408
|
+
description: "Concurrent limit for subagents",
|
|
409
|
+
submenu: true,
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
"task.maxRecursionDepth": {
|
|
413
|
+
type: "number",
|
|
414
|
+
default: 2,
|
|
415
|
+
ui: {
|
|
416
|
+
tab: "tools",
|
|
417
|
+
label: "Task max recursion depth",
|
|
418
|
+
description: "How many levels deep subagents can spawn their own subagents",
|
|
419
|
+
submenu: true,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
|
|
400
423
|
// ─────────────────────────────────────────────────────────────────────────
|
|
401
424
|
// Startup settings
|
|
402
425
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -409,10 +432,10 @@ export const SETTINGS_SCHEMA = {
|
|
|
409
432
|
// ─────────────────────────────────────────────────────────────────────────
|
|
410
433
|
// Notification settings
|
|
411
434
|
// ─────────────────────────────────────────────────────────────────────────
|
|
412
|
-
"
|
|
435
|
+
"completion.notify": {
|
|
413
436
|
type: "enum",
|
|
414
|
-
values: ["
|
|
415
|
-
default: "
|
|
437
|
+
values: ["on", "off"] as const,
|
|
438
|
+
default: "on",
|
|
416
439
|
ui: { tab: "input", label: "Completion notification", description: "Notify when the agent completes" },
|
|
417
440
|
},
|
|
418
441
|
|
|
@@ -429,10 +452,10 @@ export const SETTINGS_SCHEMA = {
|
|
|
429
452
|
submenu: true,
|
|
430
453
|
},
|
|
431
454
|
},
|
|
432
|
-
"ask.
|
|
455
|
+
"ask.notify": {
|
|
433
456
|
type: "enum",
|
|
434
|
-
values: ["
|
|
435
|
-
default: "
|
|
457
|
+
values: ["on", "off"] as const,
|
|
458
|
+
default: "on",
|
|
436
459
|
ui: { tab: "input", label: "Ask notification", description: "Notify when ask tool is waiting for input" },
|
|
437
460
|
},
|
|
438
461
|
|
package/src/config/settings.ts
CHANGED
|
@@ -449,10 +449,6 @@ export class Settings {
|
|
|
449
449
|
|
|
450
450
|
// Build merged view
|
|
451
451
|
this.rebuildMerged();
|
|
452
|
-
|
|
453
|
-
// Apply environment variables
|
|
454
|
-
this.applyEnvironmentVariables();
|
|
455
|
-
|
|
456
452
|
return this;
|
|
457
453
|
}
|
|
458
454
|
|
|
@@ -550,16 +546,6 @@ export class Settings {
|
|
|
550
546
|
return raw;
|
|
551
547
|
}
|
|
552
548
|
|
|
553
|
-
private applyEnvironmentVariables(): void {
|
|
554
|
-
const env = this.get("env") as Record<string, string>;
|
|
555
|
-
if (!env) return;
|
|
556
|
-
for (const [key, value] of Object.entries(env)) {
|
|
557
|
-
if (typeof key === "string" && typeof value === "string" && !(key in process.env)) {
|
|
558
|
-
process.env[key] = value;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
549
|
// ─────────────────────────────────────────────────────────────────────────
|
|
564
550
|
// Saving
|
|
565
551
|
// ─────────────────────────────────────────────────────────────────────────
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
// Embed package.json at build time for config
|
|
6
6
|
import packageJson from "../package.json" with { type: "json" };
|
|
7
7
|
|
|
@@ -53,7 +53,7 @@ export function getChangelogPath(): string {
|
|
|
53
53
|
|
|
54
54
|
/** Get the agent config directory (e.g., ~/.omp/agent/) */
|
|
55
55
|
export function getAgentDir(): string {
|
|
56
|
-
return
|
|
56
|
+
return $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** Get path to user's custom themes directory */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents (standard) Provider
|
|
3
|
+
*
|
|
4
|
+
* Loads user-level skills from ~/.agents/skills.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { registerProvider } from "../capability";
|
|
8
|
+
import { type Skill, skillCapability } from "../capability/skill";
|
|
9
|
+
import type { LoadContext, LoadResult } from "../capability/types";
|
|
10
|
+
import { loadSkillsFromDir } from "./helpers";
|
|
11
|
+
|
|
12
|
+
const PROVIDER_ID = "agents";
|
|
13
|
+
const DISPLAY_NAME = "Agents (standard)";
|
|
14
|
+
const PRIORITY = 70;
|
|
15
|
+
|
|
16
|
+
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
17
|
+
const userSkillsDir = path.join(ctx.home, ".agents", "skills");
|
|
18
|
+
const result = await loadSkillsFromDir(ctx, {
|
|
19
|
+
dir: userSkillsDir,
|
|
20
|
+
providerId: PROVIDER_ID,
|
|
21
|
+
level: "user",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
items: result.items,
|
|
26
|
+
warnings: result.warnings ?? [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
registerProvider<Skill>(skillCapability.id, {
|
|
31
|
+
id: PROVIDER_ID,
|
|
32
|
+
displayName: DISPLAY_NAME,
|
|
33
|
+
description: "Load skills from ~/.agents/skills",
|
|
34
|
+
priority: PRIORITY,
|
|
35
|
+
load: loadSkills,
|
|
36
|
+
});
|
package/src/discovery/index.ts
CHANGED
package/src/exa/mcp-client.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Client for interacting with Exa MCP servers.
|
|
5
5
|
*/
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { TSchema } from "@sinclair/typebox";
|
|
9
9
|
import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types";
|
|
10
10
|
import { callMCP } from "../mcp/json-rpc";
|
|
@@ -21,8 +21,8 @@ import type {
|
|
|
21
21
|
/** Find EXA_API_KEY from process.env or .env files */
|
|
22
22
|
export async function findApiKey(): Promise<string | null> {
|
|
23
23
|
// Check process.env first
|
|
24
|
-
if (
|
|
25
|
-
return
|
|
24
|
+
if ($env.EXA_API_KEY) {
|
|
25
|
+
return $env.EXA_API_KEY;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// Try loading from .env files in cwd and home
|
package/src/ipy/executor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { getAgentDir } from "../config";
|
|
4
4
|
import { OutputSink } from "../session/streaming-output";
|
|
5
5
|
import { time } from "../utils/timings";
|
|
@@ -15,9 +15,7 @@ import {
|
|
|
15
15
|
import { discoverPythonModules } from "./modules";
|
|
16
16
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
17
17
|
|
|
18
|
-
const debugStartup = process.
|
|
19
|
-
? (stage: string) => process.stderr.write(`[startup] ${stage}\n`)
|
|
20
|
-
: () => {};
|
|
18
|
+
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
21
19
|
|
|
22
20
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
23
21
|
const MAX_KERNEL_SESSIONS = 4;
|
|
@@ -327,7 +325,7 @@ async function createKernelSession(
|
|
|
327
325
|
const env: Record<string, string> | undefined =
|
|
328
326
|
sessionFile || artifactsDir
|
|
329
327
|
? {
|
|
330
|
-
...(sessionFile ? {
|
|
328
|
+
...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
|
|
331
329
|
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
332
330
|
}
|
|
333
331
|
: undefined;
|
|
@@ -384,7 +382,7 @@ async function restartKernelSession(
|
|
|
384
382
|
const env: Record<string, string> | undefined =
|
|
385
383
|
sessionFile || artifactsDir
|
|
386
384
|
? {
|
|
387
|
-
...(sessionFile ? {
|
|
385
|
+
...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
|
|
388
386
|
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
389
387
|
}
|
|
390
388
|
: undefined;
|
|
@@ -537,7 +535,7 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
537
535
|
const env: Record<string, string> | undefined =
|
|
538
536
|
sessionFile || artifactsDir
|
|
539
537
|
? {
|
|
540
|
-
...(sessionFile ? {
|
|
538
|
+
...(sessionFile ? { PI_SESSION_FILE: sessionFile } : {}),
|
|
541
539
|
...(artifactsDir ? { ARTIFACTS: artifactsDir } : {}),
|
|
542
540
|
}
|
|
543
541
|
: undefined;
|
|
@@ -243,7 +243,7 @@ async function startGatewayProcess(
|
|
|
243
243
|
const kernelEnv: Record<string, string | undefined> = {
|
|
244
244
|
...runtime.env,
|
|
245
245
|
PYTHONUNBUFFERED: "1",
|
|
246
|
-
|
|
246
|
+
PI_SHELL_SNAPSHOT: snapshotPath ?? undefined,
|
|
247
247
|
};
|
|
248
248
|
|
|
249
249
|
const gatewayPort = await allocatePort();
|
package/src/ipy/kernel.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $env, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { $ } from "bun";
|
|
3
|
-
import { nanoid } from "nanoid";
|
|
4
3
|
import { Settings } from "../config/settings";
|
|
5
4
|
import { time } from "../utils/timings";
|
|
6
5
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
@@ -11,12 +10,10 @@ import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
|
11
10
|
|
|
12
11
|
const TEXT_ENCODER = new TextEncoder();
|
|
13
12
|
const TEXT_DECODER = new TextDecoder();
|
|
14
|
-
const TRACE_IPC =
|
|
13
|
+
const TRACE_IPC = $env.PI_PYTHON_IPC_TRACE === "1";
|
|
15
14
|
const PRELUDE_INTROSPECTION_SNIPPET = "import json\nprint(json.dumps(__omp_prelude_docs__()))";
|
|
16
15
|
|
|
17
|
-
const debugStartup = process.
|
|
18
|
-
? (stage: string) => process.stderr.write(`[startup] ${stage}\n`)
|
|
19
|
-
: () => {};
|
|
16
|
+
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
20
17
|
|
|
21
18
|
class SharedGatewayCreateError extends Error {
|
|
22
19
|
readonly status: number;
|
|
@@ -33,11 +30,11 @@ interface ExternalGatewayConfig {
|
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
function getExternalGatewayConfig(): ExternalGatewayConfig | null {
|
|
36
|
-
const url =
|
|
33
|
+
const url = $env.PI_PYTHON_GATEWAY_URL;
|
|
37
34
|
if (!url) return null;
|
|
38
35
|
return {
|
|
39
36
|
url: url.replace(/\/$/, ""),
|
|
40
|
-
token:
|
|
37
|
+
token: $env.PI_PYTHON_GATEWAY_TOKEN,
|
|
41
38
|
};
|
|
42
39
|
}
|
|
43
40
|
|
|
@@ -111,7 +108,7 @@ export interface PythonKernelAvailability {
|
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
export async function checkPythonKernelAvailability(cwd: string): Promise<PythonKernelAvailability> {
|
|
114
|
-
if (process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test" ||
|
|
111
|
+
if (process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test" || $env.PI_PYTHON_SKIP_CHECK === "1") {
|
|
115
112
|
return { ok: true };
|
|
116
113
|
}
|
|
117
114
|
|
|
@@ -163,7 +160,7 @@ async function checkExternalGatewayAvailability(config: ExternalGatewayConfig):
|
|
|
163
160
|
if (response.status === 401 || response.status === 403) {
|
|
164
161
|
return {
|
|
165
162
|
ok: false,
|
|
166
|
-
reason: `External gateway at ${config.url} requires authentication. Set
|
|
163
|
+
reason: `External gateway at ${config.url} requires authentication. Set PI_PYTHON_GATEWAY_TOKEN.`,
|
|
167
164
|
};
|
|
168
165
|
}
|
|
169
166
|
|
|
@@ -379,7 +376,15 @@ export class PythonKernel {
|
|
|
379
376
|
const kernelInfo = (await createResponse.json()) as { id: string };
|
|
380
377
|
const kernelId = kernelInfo.id;
|
|
381
378
|
|
|
382
|
-
const kernel = new PythonKernel(
|
|
379
|
+
const kernel = new PythonKernel(
|
|
380
|
+
Snowflake.next(),
|
|
381
|
+
kernelId,
|
|
382
|
+
config.url,
|
|
383
|
+
Snowflake.next(),
|
|
384
|
+
"omp",
|
|
385
|
+
false,
|
|
386
|
+
config.token,
|
|
387
|
+
);
|
|
383
388
|
|
|
384
389
|
try {
|
|
385
390
|
await kernel.connectWebSocket();
|
|
@@ -427,7 +432,7 @@ export class PythonKernel {
|
|
|
427
432
|
debugStartup("sharedGateway:json:done");
|
|
428
433
|
const kernelId = kernelInfo.id;
|
|
429
434
|
|
|
430
|
-
const kernel = new PythonKernel(
|
|
435
|
+
const kernel = new PythonKernel(Snowflake.next(), kernelId, gatewayUrl, Snowflake.next(), "omp", true);
|
|
431
436
|
debugStartup("sharedGateway:kernelCreated");
|
|
432
437
|
|
|
433
438
|
try {
|
|
@@ -580,7 +585,7 @@ export class PythonKernel {
|
|
|
580
585
|
throw new Error("Python kernel is not running");
|
|
581
586
|
}
|
|
582
587
|
|
|
583
|
-
const msgId =
|
|
588
|
+
const msgId = Snowflake.next();
|
|
584
589
|
const msg: JupyterMessage = {
|
|
585
590
|
channel: "shell",
|
|
586
591
|
header: {
|
|
@@ -746,7 +751,7 @@ export class PythonKernel {
|
|
|
746
751
|
this.sendMessage({
|
|
747
752
|
channel: "stdin",
|
|
748
753
|
header: {
|
|
749
|
-
msg_id:
|
|
754
|
+
msg_id: Snowflake.next(),
|
|
750
755
|
session: this.sessionId,
|
|
751
756
|
username: this.username,
|
|
752
757
|
date: new Date().toISOString(),
|
|
@@ -808,7 +813,7 @@ export class PythonKernel {
|
|
|
808
813
|
const msg: JupyterMessage = {
|
|
809
814
|
channel: "control",
|
|
810
815
|
header: {
|
|
811
|
-
msg_id:
|
|
816
|
+
msg_id: Snowflake.next(),
|
|
812
817
|
session: this.sessionId,
|
|
813
818
|
username: this.username,
|
|
814
819
|
date: new Date().toISOString(),
|
package/src/ipy/prelude.py
CHANGED
|
@@ -640,7 +640,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
640
640
|
output('explore_0', offset=10, limit=20) # Lines 10-29
|
|
641
641
|
output('explore_0', 'reviewer_1') # Read multiple outputs
|
|
642
642
|
"""
|
|
643
|
-
session_file = os.environ.get("
|
|
643
|
+
session_file = os.environ.get("PI_SESSION_FILE")
|
|
644
644
|
if not session_file:
|
|
645
645
|
_emit_status("output", error="No session file available")
|
|
646
646
|
raise RuntimeError("No session - output artifacts unavailable")
|
package/src/ipy/runtime.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
|
|
10
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
11
|
+
|
|
10
12
|
const DEFAULT_ENV_ALLOWLIST = new Set([
|
|
11
13
|
"PATH",
|
|
12
14
|
"HOME",
|
|
@@ -76,7 +78,7 @@ const DEFAULT_ENV_DENYLIST = new Set([
|
|
|
76
78
|
"MISTRAL_API_KEY",
|
|
77
79
|
]);
|
|
78
80
|
|
|
79
|
-
const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "
|
|
81
|
+
const DEFAULT_ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "PI_"];
|
|
80
82
|
|
|
81
83
|
const CASE_INSENSITIVE_ENV = process.platform === "win32";
|
|
82
84
|
const BASE_ENV_ALLOWLIST = new Set([...DEFAULT_ENV_ALLOWLIST, ...WINDOWS_ENV_ALLOWLIST]);
|
|
@@ -107,7 +109,7 @@ export interface PythonRuntime {
|
|
|
107
109
|
/** Filtered environment variables */
|
|
108
110
|
env: Record<string, string | undefined>;
|
|
109
111
|
/** Path to virtual environment, if detected */
|
|
110
|
-
venvPath
|
|
112
|
+
venvPath?: string;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
@@ -135,15 +137,15 @@ export function filterEnv(env: Record<string, string | undefined>): Record<strin
|
|
|
135
137
|
/**
|
|
136
138
|
* Detect virtual environment path from VIRTUAL_ENV or common locations.
|
|
137
139
|
*/
|
|
138
|
-
export function resolveVenvPath(cwd: string): string |
|
|
139
|
-
if (
|
|
140
|
+
export function resolveVenvPath(cwd: string): string | undefined {
|
|
141
|
+
if ($env.VIRTUAL_ENV) return $env.VIRTUAL_ENV;
|
|
140
142
|
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
|
|
141
143
|
for (const candidate of candidates) {
|
|
142
144
|
if (fs.existsSync(candidate)) {
|
|
143
145
|
return candidate;
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
|
-
return
|
|
148
|
+
return undefined;
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
/**
|
|
@@ -189,6 +191,5 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
|
|
|
189
191
|
return {
|
|
190
192
|
pythonPath: resolveWindowlessPython(pythonPath),
|
|
191
193
|
env,
|
|
192
|
-
venvPath: null,
|
|
193
194
|
};
|
|
194
195
|
}
|
package/src/lsp/lspmux.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { TOML } from "bun";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -127,7 +127,7 @@ async function checkServerRunning(binaryPath: string): Promise<boolean> {
|
|
|
127
127
|
* Detect lspmux availability and state.
|
|
128
128
|
* Results are cached for STATE_CACHE_TTL_MS.
|
|
129
129
|
*
|
|
130
|
-
* Set
|
|
130
|
+
* Set PI_DISABLE_LSPMUX=1 to disable.
|
|
131
131
|
*/
|
|
132
132
|
export async function detectLspmux(): Promise<LspmuxState> {
|
|
133
133
|
const now = Date.now();
|
|
@@ -135,7 +135,7 @@ export async function detectLspmux(): Promise<LspmuxState> {
|
|
|
135
135
|
return cachedState;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
if (
|
|
138
|
+
if ($env.PI_DISABLE_LSPMUX === "1") {
|
|
139
139
|
cachedState = { available: false, running: false, binaryPath: null, config: null };
|
|
140
140
|
cacheTimestamp = now;
|
|
141
141
|
return cachedState;
|
package/src/main.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as os from "node:os";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { createInterface } from "node:readline/promises";
|
|
11
11
|
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
12
|
-
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import { $env, postmortem } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
15
15
|
import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
|
|
@@ -42,10 +42,8 @@ import { resolvePromptInput } from "./system-prompt";
|
|
|
42
42
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
43
43
|
import { printTimings, time } from "./utils/timings";
|
|
44
44
|
|
|
45
|
-
/** Conditional startup debug prints (stderr) when
|
|
46
|
-
const debugStartup = process.
|
|
47
|
-
? (stage: string) => process.stderr.write(`[startup] ${stage}\n`)
|
|
48
|
-
: () => {};
|
|
45
|
+
/** Conditional startup debug prints (stderr) when PI_DEBUG_STARTUP is set */
|
|
46
|
+
const debugStartup = $env.PI_DEBUG_STARTUP ? (stage: string) => process.stderr.write(`[startup] ${stage}\n`) : () => {};
|
|
49
47
|
|
|
50
48
|
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
|
51
49
|
try {
|
|
@@ -655,9 +653,9 @@ export async function main(args: string[]) {
|
|
|
655
653
|
time("initializeWithSettings");
|
|
656
654
|
|
|
657
655
|
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
658
|
-
const smolModel = parsed.smol ??
|
|
659
|
-
const slowModel = parsed.slow ??
|
|
660
|
-
const planModel = parsed.plan ??
|
|
656
|
+
const smolModel = parsed.smol ?? $env.PI_SMOL_MODEL;
|
|
657
|
+
const slowModel = parsed.slow ?? $env.PI_SLOW_MODEL;
|
|
658
|
+
const planModel = parsed.plan ?? $env.PI_PLAN_MODEL;
|
|
661
659
|
if (smolModel || slowModel || planModel) {
|
|
662
660
|
const currentRoles = settings.get("modelRoles") as Record<string, string>;
|
|
663
661
|
if (smolModel) currentRoles.smol = smolModel;
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -78,19 +78,29 @@ function formatMCPContent(content: MCPContent[]): string {
|
|
|
78
78
|
* "puppeteer_screenshot"), strips the redundant prefix to produce
|
|
79
79
|
* "mcp_puppeteer_screenshot" instead of "mcp_puppeteer_puppeteer_screenshot".
|
|
80
80
|
*/
|
|
81
|
+
function sanitizeMCPToolNamePart(value: string, fallback: string): string {
|
|
82
|
+
const sanitized = value
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z_]+/g, "_")
|
|
85
|
+
.replace(/_+/g, "_")
|
|
86
|
+
.replace(/^_+|_+$/g, "");
|
|
87
|
+
|
|
88
|
+
return sanitized.length > 0 ? sanitized : fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export function createMCPToolName(serverName: string, toolName: string): string {
|
|
92
|
+
const sanitizedServerName = sanitizeMCPToolNamePart(serverName, "server");
|
|
93
|
+
const sanitizedToolName = sanitizeMCPToolNamePart(toolName, "tool");
|
|
94
|
+
|
|
82
95
|
// Strip redundant server name prefix from tool name if present
|
|
83
|
-
const prefixWithUnderscore = `${
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
normalizedToolName = toolName.slice(prefixWithUnderscore.length);
|
|
89
|
-
} else if (toolName.startsWith(prefixWithHyphen)) {
|
|
90
|
-
normalizedToolName = toolName.slice(prefixWithHyphen.length);
|
|
96
|
+
const prefixWithUnderscore = `${sanitizedServerName}_`;
|
|
97
|
+
|
|
98
|
+
let normalizedToolName = sanitizedToolName;
|
|
99
|
+
if (sanitizedToolName.startsWith(prefixWithUnderscore)) {
|
|
100
|
+
normalizedToolName = sanitizedToolName.slice(prefixWithUnderscore.length);
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
return `mcp_${
|
|
103
|
+
return `mcp_${sanitizedServerName}_${normalizedToolName}`;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { Container, Markdown, Spacer,
|
|
2
|
+
import { Container, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import { hasPendingMermaid, prerenderMermaid } from "../../modes/theme/mermaid-cache";
|
|
4
4
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
5
5
|
|
|
@@ -38,7 +38,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
private triggerMermaidPrerender(message: AssistantMessage): void {
|
|
41
|
-
if (!
|
|
41
|
+
if (!TERMINAL.imageProtocol || this.prerenderInFlight) return;
|
|
42
42
|
|
|
43
43
|
// Check if any text content has pending mermaid blocks
|
|
44
44
|
const hasPending = message.content.some(c => c.type === "text" && c.text.trim() && hasPendingMermaid(c.text));
|
|
@@ -6,7 +6,7 @@ import * as fs from "node:fs/promises";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
9
|
-
import {
|
|
9
|
+
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { getEditorTheme, theme } from "../../modes/theme/theme";
|
|
11
11
|
import { DynamicBorder } from "./dynamic-border";
|
|
12
12
|
|
|
@@ -47,7 +47,7 @@ export class HookEditorComponent extends Container {
|
|
|
47
47
|
this.addChild(new Spacer(1));
|
|
48
48
|
|
|
49
49
|
// Add hint
|
|
50
|
-
const hasExternalEditor = !!(
|
|
50
|
+
const hasExternalEditor = !!($env.VISUAL || $env.EDITOR);
|
|
51
51
|
const hint = hasExternalEditor
|
|
52
52
|
? "ctrl+enter submit esc cancel ctrl+g external editor"
|
|
53
53
|
: "ctrl+enter submit esc cancel";
|
|
@@ -83,13 +83,13 @@ export class HookEditorComponent extends Container {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
private async openExternalEditor(): Promise<void> {
|
|
86
|
-
const editorCmd =
|
|
86
|
+
const editorCmd = $env.VISUAL || $env.EDITOR;
|
|
87
87
|
if (!editorCmd) {
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const currentText = this.editor.getText();
|
|
92
|
-
const tmpFile = path.join(os.tmpdir(), `omp-hook-editor-${
|
|
92
|
+
const tmpFile = path.join(os.tmpdir(), `omp-hook-editor-${Snowflake.next()}.md`);
|
|
93
93
|
|
|
94
94
|
try {
|
|
95
95
|
await Bun.write(tmpFile, currentText);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 1. Add it to settings-schema.ts with a `ui` field
|
|
7
7
|
* 2. That's it - it appears in the UI automatically
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import {
|
|
11
11
|
getDefault,
|
|
12
12
|
getEnumValues,
|
|
@@ -55,7 +55,7 @@ export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef;
|
|
|
55
55
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
56
56
|
|
|
57
57
|
const CONDITIONS: Record<string, () => boolean> = {
|
|
58
|
-
hasImageProtocol: () => !!
|
|
58
|
+
hasImageProtocol: () => !!TERMINAL.imageProtocol,
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -73,6 +73,25 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
73
73
|
{ value: "5", label: "5 retries" },
|
|
74
74
|
{ value: "10", label: "10 retries" },
|
|
75
75
|
],
|
|
76
|
+
// Task max concurrency
|
|
77
|
+
"task.maxConcurrency": () => [
|
|
78
|
+
{ value: "0", label: "Unlimited" },
|
|
79
|
+
{ value: "1", label: "1 task" },
|
|
80
|
+
{ value: "2", label: "2 tasks" },
|
|
81
|
+
{ value: "4", label: "4 tasks" },
|
|
82
|
+
{ value: "8", label: "8 tasks" },
|
|
83
|
+
{ value: "16", label: "16 tasks" },
|
|
84
|
+
{ value: "32", label: "32 tasks" },
|
|
85
|
+
{ value: "64", label: "64 tasks" },
|
|
86
|
+
],
|
|
87
|
+
// Task max recursion depth
|
|
88
|
+
"task.maxRecursionDepth": () => [
|
|
89
|
+
{ value: "-1", label: "Unlimited" },
|
|
90
|
+
{ value: "0", label: "None" },
|
|
91
|
+
{ value: "1", label: "Single" },
|
|
92
|
+
{ value: "2", label: "Double" },
|
|
93
|
+
{ value: "3", label: "Triple" },
|
|
94
|
+
],
|
|
76
95
|
// Todo max reminders
|
|
77
96
|
"todo.reminders.max": () => [
|
|
78
97
|
{ value: "1", label: "1 reminder" },
|
|
@@ -80,6 +99,22 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
80
99
|
{ value: "3", label: "3 reminders" },
|
|
81
100
|
{ value: "5", label: "5 reminders" },
|
|
82
101
|
],
|
|
102
|
+
// Grep context
|
|
103
|
+
"grep.contextBefore": () => [
|
|
104
|
+
{ value: "0", label: "0 lines" },
|
|
105
|
+
{ value: "1", label: "1 line" },
|
|
106
|
+
{ value: "2", label: "2 lines" },
|
|
107
|
+
{ value: "3", label: "3 lines" },
|
|
108
|
+
{ value: "5", label: "5 lines" },
|
|
109
|
+
],
|
|
110
|
+
"grep.contextAfter": () => [
|
|
111
|
+
{ value: "0", label: "0 lines" },
|
|
112
|
+
{ value: "1", label: "1 line" },
|
|
113
|
+
{ value: "2", label: "2 lines" },
|
|
114
|
+
{ value: "3", label: "3 lines" },
|
|
115
|
+
{ value: "5", label: "5 lines" },
|
|
116
|
+
{ value: "10", label: "10 lines" },
|
|
117
|
+
],
|
|
83
118
|
// Ask timeout
|
|
84
119
|
"ask.timeout": () => [
|
|
85
120
|
{ value: "0", label: "Disabled" },
|