@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +80 -79
  3. package/docs/compaction.md +182 -149
  4. package/docs/config-usage.md +141 -78
  5. package/docs/custom-tools.md +45 -16
  6. package/docs/extension-loading.md +56 -954
  7. package/docs/extensions.md +192 -51
  8. package/docs/hooks.md +109 -70
  9. package/docs/python-repl.md +52 -19
  10. package/docs/rpc.md +43 -19
  11. package/docs/sdk.md +270 -211
  12. package/docs/session-tree-plan.md +60 -417
  13. package/docs/session.md +104 -39
  14. package/docs/skills.md +59 -95
  15. package/docs/theme.md +139 -110
  16. package/docs/tree.md +42 -33
  17. package/docs/tui.md +226 -80
  18. package/package.json +8 -9
  19. package/src/capability/index.ts +3 -4
  20. package/src/cli/args.ts +4 -4
  21. package/src/cli/grep-cli.ts +1 -1
  22. package/src/commit/agentic/index.ts +4 -3
  23. package/src/commit/git/index.ts +2 -3
  24. package/src/commit/map-reduce/index.ts +2 -1
  25. package/src/config/prompt-templates.ts +2 -0
  26. package/src/config/settings-schema.ts +30 -7
  27. package/src/config/settings.ts +0 -14
  28. package/src/config.ts +2 -2
  29. package/src/discovery/agents.ts +36 -0
  30. package/src/discovery/index.ts +1 -0
  31. package/src/exa/mcp-client.ts +3 -3
  32. package/src/ipy/executor.ts +5 -7
  33. package/src/ipy/gateway-coordinator.ts +1 -1
  34. package/src/ipy/kernel.ts +20 -15
  35. package/src/ipy/prelude.py +1 -1
  36. package/src/ipy/runtime.ts +7 -6
  37. package/src/lsp/lspmux.ts +3 -3
  38. package/src/main.ts +6 -8
  39. package/src/mcp/tool-bridge.ts +19 -9
  40. package/src/modes/components/assistant-message.ts +2 -2
  41. package/src/modes/components/hook-editor.ts +4 -4
  42. package/src/modes/components/settings-defs.ts +37 -2
  43. package/src/modes/components/tool-execution.ts +7 -7
  44. package/src/modes/controllers/command-controller.ts +2 -2
  45. package/src/modes/controllers/event-controller.ts +4 -7
  46. package/src/modes/controllers/input-controller.ts +4 -4
  47. package/src/modes/controllers/selector-controller.ts +1 -0
  48. package/src/modes/interactive-mode.ts +3 -5
  49. package/src/modes/rpc/rpc-mode.ts +8 -9
  50. package/src/patch/index.ts +6 -6
  51. package/src/prompts/agents/explore.md +2 -2
  52. package/src/prompts/agents/frontmatter.md +5 -5
  53. package/src/prompts/agents/plan.md +3 -2
  54. package/src/prompts/agents/reviewer.md +1 -1
  55. package/src/prompts/system/system-prompt.md +1 -3
  56. package/src/sdk.ts +13 -9
  57. package/src/session/agent-session.ts +6 -4
  58. package/src/session/compaction/compaction.ts +3 -3
  59. package/src/session/session-manager.ts +8 -9
  60. package/src/ssh/connection-manager.ts +4 -4
  61. package/src/system-prompt.ts +2 -6
  62. package/src/task/agents.ts +1 -1
  63. package/src/task/executor.ts +31 -8
  64. package/src/task/index.ts +14 -35
  65. package/src/task/omp-command.ts +3 -1
  66. package/src/task/output-manager.ts +20 -6
  67. package/src/task/parallel.ts +3 -3
  68. package/src/task/render.ts +16 -2
  69. package/src/task/types.ts +13 -20
  70. package/src/task/worktree.ts +3 -3
  71. package/src/tools/ask.ts +3 -8
  72. package/src/tools/fetch.ts +2 -2
  73. package/src/tools/gemini-image.ts +5 -6
  74. package/src/tools/grep.ts +5 -5
  75. package/src/tools/index.ts +12 -5
  76. package/src/tools/read.ts +1 -1
  77. package/src/tools/todo-write.ts +2 -3
  78. package/src/utils/frontmatter.ts +1 -1
  79. package/src/utils/image-resize.ts +1 -1
  80. package/src/utils/timings.ts +3 -2
  81. package/src/web/scrapers/github.ts +2 -2
  82. package/src/web/scrapers/utils.ts +2 -3
  83. package/src/web/scrapers/youtube.ts +2 -3
  84. package/src/web/search/auth.ts +5 -6
  85. package/src/web/search/providers/anthropic.ts +3 -2
  86. 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
- "notifications.onComplete": {
435
+ "completion.notify": {
413
436
  type: "enum",
414
- values: ["auto", "bell", "osc99", "osc9", "off"] as const,
415
- default: "auto",
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.notification": {
455
+ "ask.notify": {
433
456
  type: "enum",
434
- values: ["auto", "bell", "osc99", "osc9", "off"] as const,
435
- default: "auto",
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
 
@@ -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 process.env.OMP_CODING_AGENT_DIR || path.join(os.homedir(), CONFIG_DIR_NAME, "agent");
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
+ });
@@ -24,6 +24,7 @@ import "./agents-md";
24
24
  import "./builtin";
25
25
  import "./claude";
26
26
  import "./cline";
27
+ import "./agents";
27
28
  import "./codex";
28
29
  import "./cursor";
29
30
  import "./gemini";
@@ -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 (process.env.EXA_API_KEY) {
25
- return process.env.EXA_API_KEY;
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
@@ -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.env.OMP_DEBUG_STARTUP
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 ? { OMP_SESSION_FILE: 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 ? { OMP_SESSION_FILE: 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 ? { OMP_SESSION_FILE: 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
- OMP_SHELL_SNAPSHOT: snapshotPath ?? undefined,
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 = process.env.OMP_PYTHON_IPC_TRACE === "1";
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.env.OMP_DEBUG_STARTUP
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 = process.env.OMP_PYTHON_GATEWAY_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: process.env.OMP_PYTHON_GATEWAY_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" || process.env.OMP_PYTHON_SKIP_CHECK === "1") {
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 OMP_PYTHON_GATEWAY_TOKEN.`,
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(nanoid(), kernelId, config.url, nanoid(), "omp", false, config.token);
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(nanoid(), kernelId, gatewayUrl, nanoid(), "omp", true);
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 = nanoid();
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: nanoid(),
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: nanoid(),
816
+ msg_id: Snowflake.next(),
812
817
  session: this.sessionId,
813
818
  username: this.username,
814
819
  date: new Date().toISOString(),
@@ -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("OMP_SESSION_FILE")
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")
@@ -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_", "OMP_"];
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: string | null;
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 | null {
139
- if (process.env.VIRTUAL_ENV) return process.env.VIRTUAL_ENV;
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 null;
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 OMP_DISABLE_LSPMUX=1 to disable.
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 (process.env.OMP_DISABLE_LSPMUX === "1") {
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 OMP_DEBUG_STARTUP is set */
46
- const debugStartup = process.env.OMP_DEBUG_STARTUP
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 ?? process.env.OMP_SMOL_MODEL;
659
- const slowModel = parsed.slow ?? process.env.OMP_SLOW_MODEL;
660
- const planModel = parsed.plan ?? process.env.OMP_PLAN_MODEL;
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;
@@ -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 = `${serverName}_`;
84
- const prefixWithHyphen = `${serverName}-`;
85
-
86
- let normalizedToolName = toolName;
87
- if (toolName.startsWith(prefixWithUnderscore)) {
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_${serverName}_${normalizedToolName}`;
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, TERMINAL_INFO, Text } from "@oh-my-pi/pi-tui";
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 (!TERMINAL_INFO.imageProtocol || this.prerenderInFlight) return;
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 { nanoid } from "nanoid";
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 = !!(process.env.VISUAL || process.env.EDITOR);
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 = process.env.VISUAL || process.env.EDITOR;
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-${nanoid()}.md`);
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 { TERMINAL_INFO } from "@oh-my-pi/pi-tui";
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: () => !!TERMINAL_INFO.imageProtocol,
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" },