@oh-my-pi/pi-coding-agent 6.7.670 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +6 -7
  3. package/src/cli/session-picker.ts +27 -28
  4. package/src/cli/setup-cli.ts +7 -16
  5. package/src/cli/update-cli.ts +1 -1
  6. package/src/config.ts +1 -1
  7. package/src/core/agent-session.ts +202 -37
  8. package/src/core/agent-storage.ts +1 -1
  9. package/src/core/auth-storage.ts +15 -25
  10. package/src/core/bash-executor.ts +63 -105
  11. package/src/core/custom-commands/loader.ts +1 -1
  12. package/src/core/custom-tools/loader.ts +1 -1
  13. package/src/core/custom-tools/types.ts +1 -2
  14. package/src/core/exec.ts +16 -100
  15. package/src/core/extensions/index.ts +1 -7
  16. package/src/core/extensions/loader.ts +1 -1
  17. package/src/core/extensions/runner.ts +1 -1
  18. package/src/core/extensions/types.ts +2 -2
  19. package/src/core/extensions/wrapper.ts +15 -20
  20. package/src/core/frontmatter.ts +1 -1
  21. package/src/core/history-storage.ts +3 -6
  22. package/src/core/hooks/index.ts +2 -2
  23. package/src/core/hooks/loader.ts +1 -1
  24. package/src/core/hooks/tool-wrapper.ts +14 -26
  25. package/src/core/hooks/types.ts +1 -2
  26. package/src/core/keybindings.ts +1 -1
  27. package/src/core/mcp/client.ts +13 -13
  28. package/src/core/mcp/json-rpc.ts +1 -1
  29. package/src/core/mcp/loader.ts +1 -1
  30. package/src/core/mcp/manager.ts +2 -2
  31. package/src/core/mcp/tool-cache.ts +1 -1
  32. package/src/core/mcp/transports/http.ts +32 -70
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/plugins/installer.ts +13 -11
  35. package/src/core/prompt-templates.ts +4 -9
  36. package/src/core/python-executor.ts +23 -18
  37. package/src/core/python-gateway-coordinator.ts +29 -28
  38. package/src/core/python-kernel.ts +230 -211
  39. package/src/core/sdk.ts +10 -13
  40. package/src/core/session-manager.ts +1 -1
  41. package/src/core/settings-manager.ts +22 -9
  42. package/src/core/skills.ts +1 -1
  43. package/src/core/ssh/connection-manager.ts +19 -33
  44. package/src/core/ssh/ssh-executor.ts +39 -35
  45. package/src/core/ssh/sshfs-mount.ts +14 -33
  46. package/src/core/storage-migration.ts +1 -1
  47. package/src/core/streaming-output.ts +183 -127
  48. package/src/core/system-prompt.ts +119 -79
  49. package/src/core/title-generator.ts +1 -1
  50. package/src/core/tools/ask.ts +2 -2
  51. package/src/core/tools/bash.ts +3 -3
  52. package/src/core/tools/calculator.ts +1 -1
  53. package/src/core/tools/exa/mcp-client.ts +1 -1
  54. package/src/core/tools/exa/render.ts +1 -1
  55. package/src/core/tools/find.ts +39 -71
  56. package/src/core/tools/gemini-image.ts +1 -1
  57. package/src/core/tools/grep.ts +88 -100
  58. package/src/core/tools/index.ts +1 -1
  59. package/src/core/tools/ls.ts +1 -1
  60. package/src/core/tools/lsp/client.ts +50 -50
  61. package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
  62. package/src/core/tools/lsp/config.ts +1 -1
  63. package/src/core/tools/lsp/index.ts +2 -4
  64. package/src/core/tools/lsp/lspmux.ts +1 -1
  65. package/src/core/tools/lsp/rust-analyzer.ts +2 -2
  66. package/src/core/tools/lsp/utils.ts +0 -14
  67. package/src/core/tools/notebook.ts +1 -1
  68. package/src/core/tools/patch/shared.ts +3 -4
  69. package/src/core/tools/python.ts +3 -3
  70. package/src/core/tools/read.ts +29 -68
  71. package/src/core/tools/render-utils.ts +0 -5
  72. package/src/core/tools/ssh.ts +3 -3
  73. package/src/core/tools/task/model-resolver.ts +7 -9
  74. package/src/core/tools/task/worker.ts +144 -139
  75. package/src/core/tools/todo-write.ts +1 -1
  76. package/src/core/tools/truncate.ts +2 -2
  77. package/src/core/tools/web-fetch.ts +13 -15
  78. package/src/core/tools/web-scrapers/types.ts +1 -3
  79. package/src/core/tools/web-scrapers/utils.ts +14 -13
  80. package/src/core/tools/web-scrapers/youtube.ts +39 -12
  81. package/src/core/tools/web-search/auth.ts +1 -1
  82. package/src/core/tools/write.ts +1 -1
  83. package/src/core/ttsr.ts +1 -1
  84. package/src/core/utils.ts +1 -187
  85. package/src/core/voice-controller.ts +1 -1
  86. package/src/core/voice-supervisor.ts +11 -38
  87. package/src/core/voice.ts +1 -8
  88. package/src/discovery/codex.ts +1 -1
  89. package/src/index.ts +4 -4
  90. package/src/main.ts +5 -10
  91. package/src/migrations.ts +1 -1
  92. package/src/modes/index.ts +7 -40
  93. package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
  94. package/src/modes/interactive/components/hook-editor.ts +12 -9
  95. package/src/modes/interactive/components/login-dialog.ts +24 -11
  96. package/src/modes/interactive/components/settings-defs.ts +9 -0
  97. package/src/modes/interactive/components/status-line.ts +36 -35
  98. package/src/modes/interactive/components/todo-display.ts +1 -1
  99. package/src/modes/interactive/components/tool-execution.ts +1 -1
  100. package/src/modes/interactive/controllers/command-controller.ts +50 -84
  101. package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
  102. package/src/modes/interactive/controllers/input-controller.ts +12 -11
  103. package/src/modes/interactive/interactive-mode.ts +10 -11
  104. package/src/modes/interactive/theme/theme.ts +1 -1
  105. package/src/modes/interactive/types.ts +1 -1
  106. package/src/modes/rpc/rpc-client.ts +91 -121
  107. package/src/modes/rpc/rpc-mode.ts +71 -79
  108. package/src/prompts/system/ttsr-interrupt.md +7 -0
  109. package/src/utils/clipboard.ts +57 -141
  110. package/src/utils/shell-snapshot.ts +12 -60
  111. package/src/utils/shell.ts +35 -56
  112. package/src/utils/tools-manager.ts +42 -71
  113. package/src/core/logger.ts +0 -111
  114. package/src/modes/cleanup.ts +0 -23
@@ -1,4 +1,6 @@
1
- import { accessSync, constants, existsSync } from "node:fs";
1
+ import { constants } from "node:fs";
2
+ import { access } from "node:fs/promises";
3
+ import { $ } from "bun";
2
4
  import { SettingsManager } from "../core/settings-manager";
3
5
 
4
6
  export interface ShellConfig {
@@ -13,9 +15,9 @@ let cachedShellConfig: ShellConfig | null = null;
13
15
  /**
14
16
  * Check if a shell binary is executable.
15
17
  */
16
- function isExecutable(path: string): boolean {
18
+ async function isExecutable(path: string): Promise<boolean> {
17
19
  try {
18
- accessSync(path, constants.X_OK);
20
+ await access(path, constants.X_OK);
19
21
  return true;
20
22
  } catch {
21
23
  return false;
@@ -59,13 +61,7 @@ function getShellPrefix(): string | undefined {
59
61
  */
60
62
  function findBashOnPath(): string | null {
61
63
  try {
62
- const result = Bun.spawnSync(["where", "bash.exe"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
63
- if (result.exitCode === 0 && result.stdout) {
64
- const firstMatch = result.stdout.toString().trim().split(/\r?\n/)[0];
65
- if (firstMatch && existsSync(firstMatch)) {
66
- return firstMatch;
67
- }
68
- }
64
+ return Bun.which("bash.exe");
69
65
  } catch {
70
66
  // Ignore errors
71
67
  }
@@ -102,7 +98,7 @@ export async function getShellConfig(): Promise<ShellConfig> {
102
98
 
103
99
  // 1. Check user-specified shell path
104
100
  if (customShellPath) {
105
- if (existsSync(customShellPath)) {
101
+ if (await Bun.file(customShellPath).exists()) {
106
102
  cachedShellConfig = buildConfig(customShellPath);
107
103
  return cachedShellConfig;
108
104
  }
@@ -124,7 +120,7 @@ export async function getShellConfig(): Promise<ShellConfig> {
124
120
  }
125
121
 
126
122
  for (const path of paths) {
127
- if (existsSync(path)) {
123
+ if (await Bun.file(path).exists()) {
128
124
  cachedShellConfig = buildConfig(path);
129
125
  return cachedShellConfig;
130
126
  }
@@ -149,7 +145,7 @@ export async function getShellConfig(): Promise<ShellConfig> {
149
145
  // Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
150
146
  const userShell = process.env.SHELL;
151
147
  const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
152
- if (isValidShell && isExecutable(userShell)) {
148
+ if (isValidShell && (await isExecutable(userShell))) {
153
149
  cachedShellConfig = buildConfig(userShell);
154
150
  return cachedShellConfig;
155
151
  }
@@ -162,7 +158,7 @@ export async function getShellConfig(): Promise<ShellConfig> {
162
158
  for (const shellName of shellOrder) {
163
159
  for (const dir of fallbackPaths) {
164
160
  const shellPath = `${dir}/${shellName}`;
165
- if (isExecutable(shellPath)) {
161
+ if (await isExecutable(shellPath)) {
166
162
  cachedShellConfig = buildConfig(shellPath);
167
163
  return cachedShellConfig;
168
164
  }
@@ -181,23 +177,17 @@ export async function getShellConfig(): Promise<ShellConfig> {
181
177
  return cachedShellConfig;
182
178
  }
183
179
 
184
- let pgrepAvailable: boolean | null = null;
180
+ let pgrepAvailable: string | null | undefined;
185
181
 
186
182
  /**
187
183
  * Check if pgrep is available on this system (cached).
188
184
  */
189
- function hasPgrep(): boolean {
190
- if (pgrepAvailable === null) {
185
+ function hasPgrep(): string | null {
186
+ if (pgrepAvailable === undefined) {
191
187
  try {
192
- const result = Bun.spawnSync(["pgrep", "--version"], {
193
- stdin: "ignore",
194
- stdout: "ignore",
195
- stderr: "ignore",
196
- });
197
- // pgrep exists if it ran (exit 0 or 1 are both valid)
198
- pgrepAvailable = result.exitCode !== null;
188
+ pgrepAvailable = Bun.which("pgrep") ?? null;
199
189
  } catch {
200
- pgrepAvailable = false;
190
+ pgrepAvailable = null;
201
191
  }
202
192
  }
203
193
  return pgrepAvailable;
@@ -206,17 +196,14 @@ function hasPgrep(): boolean {
206
196
  /**
207
197
  * Get direct children of a PID using pgrep.
208
198
  */
209
- function getChildrenViaPgrep(pid: number): number[] {
210
- const result = Bun.spawnSync(["pgrep", "-P", String(pid)], {
211
- stdin: "ignore",
212
- stdout: "pipe",
213
- stderr: "ignore",
214
- });
215
-
216
- if (result.exitCode !== 0 || !result.stdout) return [];
199
+ async function getChildrenViaPgrep(pid: number): Promise<number[]> {
200
+ const result = await $`pgrep -P ${pid}`.quiet().nothrow();
201
+ if (result.exitCode !== 0) return [];
202
+ const output = result.stdout.toString().trim();
203
+ if (!output) return [];
217
204
 
218
205
  const children: number[] = [];
219
- for (const line of result.stdout.toString().trim().split("\n")) {
206
+ for (const line of output.split("\n")) {
220
207
  const childPid = parseInt(line, 10);
221
208
  if (!Number.isNaN(childPid)) children.push(childPid);
222
209
  }
@@ -226,20 +213,16 @@ function getChildrenViaPgrep(pid: number): number[] {
226
213
  /**
227
214
  * Get direct children of a PID using /proc (Linux only).
228
215
  */
229
- function getChildrenViaProc(pid: number): number[] {
216
+ async function getChildrenViaProc(pid: number): Promise<number[]> {
230
217
  try {
231
- const result = Bun.spawnSync(
232
- [
233
- "sh",
234
- "-c",
235
- `for p in /proc/[0-9]*/stat; do cat "$p" 2>/dev/null; done | awk -v ppid=${pid} '$4 == ppid { print $1 }'`,
236
- ],
237
- { stdin: "ignore", stdout: "pipe", stderr: "ignore" },
238
- );
239
- if (result.exitCode !== 0 || !result.stdout) return [];
218
+ const script = `for p in /proc/[0-9]*/stat; do cat "$p" 2>/dev/null; done | awk -v ppid=${pid} '$4 == ppid { print $1 }'`;
219
+ const result = await $`sh -c ${script}`.quiet().nothrow();
220
+ if (result.exitCode !== 0) return [];
221
+ const output = result.stdout.toString().trim();
222
+ if (!output) return [];
240
223
 
241
224
  const children: number[] = [];
242
- for (const line of result.stdout.toString().trim().split("\n")) {
225
+ for (const line of output.split("\n")) {
243
226
  const childPid = parseInt(line, 10);
244
227
  if (!Number.isNaN(childPid)) children.push(childPid);
245
228
  }
@@ -253,14 +236,14 @@ function getChildrenViaProc(pid: number): number[] {
253
236
  * Collect all descendant PIDs breadth-first.
254
237
  * Returns deepest descendants first (reverse BFS order) for proper kill ordering.
255
238
  */
256
- function getDescendantPids(pid: number): number[] {
239
+ async function getDescendantPids(pid: number): Promise<number[]> {
257
240
  const getChildren = hasPgrep() ? getChildrenViaPgrep : getChildrenViaProc;
258
241
  const descendants: number[] = [];
259
242
  const queue = [pid];
260
243
 
261
244
  while (queue.length > 0) {
262
245
  const current = queue.shift()!;
263
- const children = getChildren(current);
246
+ const children = await getChildren(current);
264
247
  for (const child of children) {
265
248
  descendants.push(child);
266
249
  queue.push(child);
@@ -284,13 +267,9 @@ function tryKill(pid: number, signal: NodeJS.Signals): boolean {
284
267
  * Kill a process and all its descendants.
285
268
  * @param gracePeriodMs - Time to wait after SIGTERM before SIGKILL (0 = immediate SIGKILL)
286
269
  */
287
- export function killProcessTree(pid: number, gracePeriodMs = 0): void {
270
+ export async function killProcessTree(pid: number, gracePeriodMs = 0): Promise<void> {
288
271
  if (process.platform === "win32") {
289
- Bun.spawnSync(["taskkill", "/F", "/T", "/PID", String(pid)], {
290
- stdin: "ignore",
291
- stdout: "ignore",
292
- stderr: "ignore",
293
- });
272
+ await $`taskkill /F /T /PID ${pid}`.quiet().nothrow();
294
273
  return;
295
274
  }
296
275
 
@@ -300,7 +279,7 @@ export function killProcessTree(pid: number, gracePeriodMs = 0): void {
300
279
  try {
301
280
  process.kill(-pid, signal);
302
281
  if (gracePeriodMs > 0) {
303
- Bun.sleepSync(gracePeriodMs);
282
+ await Bun.sleep(gracePeriodMs);
304
283
  try {
305
284
  process.kill(-pid, "SIGKILL");
306
285
  } catch {
@@ -313,11 +292,11 @@ export function killProcessTree(pid: number, gracePeriodMs = 0): void {
313
292
  }
314
293
 
315
294
  // Collect descendants BEFORE killing to minimize race window
316
- const allPids = [...getDescendantPids(pid), pid];
295
+ const allPids = [...(await getDescendantPids(pid)), pid];
317
296
 
318
297
  if (gracePeriodMs > 0) {
319
298
  for (const p of allPids) tryKill(p, "SIGTERM");
320
- Bun.sleepSync(gracePeriodMs);
299
+ await Bun.sleep(gracePeriodMs);
321
300
  }
322
301
 
323
302
  for (const p of allPids) tryKill(p, "SIGKILL");
@@ -1,7 +1,8 @@
1
- import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
1
+ import { chmod, mkdir, rename, rm } from "node:fs/promises";
2
2
  import { arch, platform } from "node:os";
3
3
  import { join } from "node:path";
4
- import chalk from "chalk";
4
+ import { createTempDir, logger } from "@oh-my-pi/pi-utils";
5
+ import { $ } from "bun";
5
6
  import { APP_NAME, getBinDir } from "../config";
6
7
 
7
8
  const TOOLS_DIR = getBinDir();
@@ -133,19 +134,14 @@ const PYTHON_TOOLS: Record<string, PythonToolConfig> = {
133
134
  },
134
135
  };
135
136
 
136
- // Check if a command exists in PATH
137
- function commandExists(cmd: string): string | null {
138
- return Bun.which(cmd);
139
- }
140
-
141
137
  export type ToolName = "fd" | "rg" | "sd" | "sg" | "yt-dlp" | "markitdown" | "html2text";
142
138
 
143
139
  // Get the path to a tool (system-wide or in our tools dir)
144
- export function getToolPath(tool: ToolName): string | null {
140
+ export async function getToolPath(tool: ToolName): Promise<string | null> {
145
141
  // Check Python tools first
146
142
  const pythonConfig = PYTHON_TOOLS[tool];
147
143
  if (pythonConfig) {
148
- return commandExists(pythonConfig.binaryName);
144
+ return Bun.which(pythonConfig.binaryName);
149
145
  }
150
146
 
151
147
  const config = TOOLS[tool];
@@ -153,12 +149,12 @@ export function getToolPath(tool: ToolName): string | null {
153
149
 
154
150
  // Check our tools directory first
155
151
  const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
156
- if (existsSync(localPath)) {
152
+ if (await Bun.file(localPath).exists()) {
157
153
  return localPath;
158
154
  }
159
155
 
160
156
  // Check system PATH
161
- return commandExists(config.binaryName);
157
+ return Bun.which(config.binaryName);
162
158
  }
163
159
 
164
160
  // Fetch latest release version from GitHub
@@ -178,27 +174,12 @@ async function getLatestVersion(repo: string): Promise<string> {
178
174
  // Download a file from URL
179
175
  async function downloadFile(url: string, dest: string): Promise<void> {
180
176
  const response = await fetch(url);
181
-
182
177
  if (!response.ok) {
183
178
  throw new Error(`Failed to download: ${response.status}`);
184
- }
185
-
186
- if (!response.body) {
179
+ } else if (!response.body) {
187
180
  throw new Error("No response body");
188
181
  }
189
-
190
- const fileStream = createWriteStream(dest);
191
- const reader = response.body.getReader();
192
- while (true) {
193
- const { done, value } = await reader.read();
194
- if (done) break;
195
- fileStream.write(Buffer.from(value));
196
- }
197
- fileStream.end();
198
- await new Promise<void>((resolve, reject) => {
199
- fileStream.on("finish", resolve);
200
- fileStream.on("error", reject);
201
- });
182
+ await Bun.write(dest, response);
202
183
  }
203
184
 
204
185
  // Download and install a tool
@@ -219,7 +200,7 @@ async function downloadTool(tool: ToolName): Promise<string> {
219
200
  }
220
201
 
221
202
  // Create tools directory
222
- mkdirSync(TOOLS_DIR, { recursive: true });
203
+ await mkdir(TOOLS_DIR, { recursive: true });
223
204
 
224
205
  const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;
225
206
  const binaryExt = plat === "win32" ? ".exe" : "";
@@ -229,7 +210,7 @@ async function downloadTool(tool: ToolName): Promise<string> {
229
210
  if (config.isDirectBinary) {
230
211
  await downloadFile(downloadUrl, binaryPath);
231
212
  if (plat !== "win32") {
232
- chmodSync(binaryPath, 0o755);
213
+ await chmod(binaryPath, 0o755);
233
214
  }
234
215
  return binaryPath;
235
216
  }
@@ -239,74 +220,62 @@ async function downloadTool(tool: ToolName): Promise<string> {
239
220
  await downloadFile(downloadUrl, archivePath);
240
221
 
241
222
  // Extract
242
- const extractDir = join(TOOLS_DIR, "extract_tmp");
243
- mkdirSync(extractDir, { recursive: true });
223
+ const tmp = await createTempDir("@omp-tools-extract-");
244
224
 
245
225
  try {
246
226
  if (assetName.endsWith(".tar.gz")) {
247
- Bun.spawnSync(["tar", "xzf", archivePath, "-C", extractDir], {
248
- stdin: "ignore",
249
- stdout: "pipe",
250
- stderr: "pipe",
251
- });
227
+ const archive = new Bun.Archive(await Bun.file(archivePath).arrayBuffer());
228
+ const files = await archive.files();
229
+ for (const [path, file] of files) {
230
+ await Bun.write(join(tmp.path, path), file);
231
+ }
252
232
  } else if (assetName.endsWith(".zip")) {
253
- Bun.spawnSync(["unzip", "-o", archivePath, "-d", extractDir], {
254
- stdin: "ignore",
255
- stdout: "pipe",
256
- stderr: "pipe",
257
- });
233
+ await mkdir(tmp.path, { recursive: true });
234
+ await $`unzip -o ${archivePath} -d ${tmp.path}`.quiet().nothrow();
258
235
  }
259
236
 
260
237
  // Find the binary in extracted files
261
238
  // ast-grep releases the binary directly in the zip, not in a subdirectory
262
239
  let extractedBinary: string;
263
240
  if (tool === "sg") {
264
- extractedBinary = join(extractDir, config.binaryName + binaryExt);
241
+ extractedBinary = join(tmp.path, config.binaryName + binaryExt);
265
242
  } else {
266
- const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, ""));
243
+ const extractedDir = join(tmp.path, assetName.replace(/\.(tar\.gz|zip)$/, ""));
267
244
  extractedBinary = join(extractedDir, config.binaryName + binaryExt);
268
245
  }
269
246
 
270
- if (existsSync(extractedBinary)) {
271
- renameSync(extractedBinary, binaryPath);
247
+ if (await Bun.file(extractedBinary).exists()) {
248
+ await rename(extractedBinary, binaryPath);
272
249
  } else {
273
250
  throw new Error(`Binary not found in archive: ${extractedBinary}`);
274
251
  }
275
252
 
276
253
  // Make executable (Unix only)
277
254
  if (plat !== "win32") {
278
- chmodSync(binaryPath, 0o755);
255
+ await chmod(binaryPath, 0o755);
279
256
  }
280
257
  } finally {
281
258
  // Cleanup
282
- rmSync(archivePath, { force: true });
283
- rmSync(extractDir, { recursive: true, force: true });
259
+ await tmp.remove();
260
+ await rm(archivePath, { force: true });
284
261
  }
285
262
 
286
263
  return binaryPath;
287
264
  }
288
265
 
289
266
  // Install a Python package via uv (preferred) or pip
290
- function installPythonPackage(pkg: string): boolean {
267
+ async function installPythonPackage(pkg: string): Promise<boolean> {
291
268
  // Try uv first (faster, better isolation)
292
- const uv = commandExists("uv");
269
+ const uv = Bun.which("uv");
293
270
  if (uv) {
294
- const result = Bun.spawnSync([uv, "tool", "install", pkg], {
295
- stdin: "ignore",
296
- stdout: "pipe",
297
- stderr: "pipe",
298
- });
271
+ const result = await $`${uv} tool install ${pkg}`.quiet().nothrow();
299
272
  if (result.exitCode === 0) return true;
300
273
  }
301
274
 
302
275
  // Fall back to pip
303
- const pip = commandExists("pip3") || commandExists("pip");
276
+ const pip = Bun.which("pip3") || Bun.which("pip");
304
277
  if (pip) {
305
- const result = Bun.spawnSync([pip, "install", "--user", pkg], {
306
- stdin: "ignore",
307
- stdout: "pipe",
308
- stderr: "pipe",
309
- });
278
+ const result = await $`${pip} install --user ${pkg}`.quiet().nothrow();
310
279
  return result.exitCode === 0;
311
280
  }
312
281
 
@@ -316,7 +285,7 @@ function installPythonPackage(pkg: string): boolean {
316
285
  // Ensure a tool is available, downloading if necessary
317
286
  // Returns the path to the tool, or null if unavailable
318
287
  export async function ensureTool(tool: ToolName, silent: boolean = false): Promise<string | undefined> {
319
- const existingPath = getToolPath(tool);
288
+ const existingPath = await getToolPath(tool);
320
289
  if (existingPath) {
321
290
  return existingPath;
322
291
  }
@@ -325,21 +294,21 @@ export async function ensureTool(tool: ToolName, silent: boolean = false): Promi
325
294
  const pythonConfig = PYTHON_TOOLS[tool];
326
295
  if (pythonConfig) {
327
296
  if (!silent) {
328
- console.log(chalk.dim(`${pythonConfig.name} not found. Installing via uv/pip...`));
297
+ logger.debug(`${pythonConfig.name} not found. Installing via uv/pip...`);
329
298
  }
330
- const success = installPythonPackage(pythonConfig.package);
299
+ const success = await installPythonPackage(pythonConfig.package);
331
300
  if (success) {
332
301
  // Re-check for the command after installation
333
- const path = commandExists(pythonConfig.binaryName);
302
+ const path = Bun.which(pythonConfig.binaryName);
334
303
  if (path) {
335
304
  if (!silent) {
336
- console.log(chalk.dim(`${pythonConfig.name} installed successfully`));
305
+ logger.debug(`${pythonConfig.name} installed successfully`);
337
306
  }
338
307
  return path;
339
308
  }
340
309
  }
341
310
  if (!silent) {
342
- console.log(chalk.yellow(`Failed to install ${pythonConfig.name}`));
311
+ logger.warn(`Failed to install ${pythonConfig.name}`);
343
312
  }
344
313
  return undefined;
345
314
  }
@@ -349,18 +318,20 @@ export async function ensureTool(tool: ToolName, silent: boolean = false): Promi
349
318
 
350
319
  // Tool not found - download it
351
320
  if (!silent) {
352
- console.log(chalk.dim(`${config.name} not found. Downloading...`));
321
+ logger.debug(`${config.name} not found. Downloading...`);
353
322
  }
354
323
 
355
324
  try {
356
325
  const path = await downloadTool(tool);
357
326
  if (!silent) {
358
- console.log(chalk.dim(`${config.name} installed to ${path}`));
327
+ logger.debug(`${config.name} installed to ${path}`);
359
328
  }
360
329
  return path;
361
330
  } catch (e) {
362
331
  if (!silent) {
363
- console.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
332
+ logger.warn(`Failed to download ${config.name}`, {
333
+ error: e instanceof Error ? e.message : String(e),
334
+ });
364
335
  }
365
336
  return undefined;
366
337
  }
@@ -1,111 +0,0 @@
1
- /**
2
- * Centralized file logger for omp.
3
- *
4
- * Logs to ~/.omp/logs/ with size-based rotation, supporting concurrent omp instances.
5
- * Each log entry includes process.pid for traceability.
6
- */
7
-
8
- import { existsSync, mkdirSync } from "node:fs";
9
- import { homedir } from "node:os";
10
- import { join } from "node:path";
11
- import winston from "winston";
12
- import DailyRotateFile from "winston-daily-rotate-file";
13
-
14
- /** Get the logs directory (~/.omp/logs/) */
15
- function getLogsDir(): string {
16
- return join(homedir(), ".omp", "logs");
17
- }
18
-
19
- /** Ensure logs directory exists */
20
- function ensureLogsDir(): string {
21
- const logsDir = getLogsDir();
22
- if (!existsSync(logsDir)) {
23
- mkdirSync(logsDir, { recursive: true });
24
- }
25
- return logsDir;
26
- }
27
-
28
- /** Custom format that includes pid and flattens metadata */
29
- const logFormat = winston.format.combine(
30
- winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
31
- winston.format.printf(({ timestamp, level, message, ...meta }) => {
32
- const entry: Record<string, unknown> = {
33
- timestamp,
34
- level,
35
- pid: process.pid,
36
- message,
37
- };
38
- // Flatten metadata into entry
39
- for (const [key, value] of Object.entries(meta)) {
40
- if (key !== "level" && key !== "timestamp" && key !== "message") {
41
- entry[key] = value;
42
- }
43
- }
44
- return JSON.stringify(entry);
45
- }),
46
- );
47
-
48
- /** Size-based rotating file transport */
49
- const fileTransport = new DailyRotateFile({
50
- dirname: ensureLogsDir(),
51
- filename: "omp.%DATE%.log",
52
- datePattern: "YYYY-MM-DD",
53
- maxSize: "10m",
54
- maxFiles: 5,
55
- zippedArchive: true,
56
- });
57
-
58
- /** The winston logger instance */
59
- const winstonLogger = winston.createLogger({
60
- level: "debug",
61
- format: logFormat,
62
- transports: [fileTransport],
63
- // Don't exit on error - logging failures shouldn't crash the app
64
- exitOnError: false,
65
- });
66
-
67
- /** Logger type exposed to plugins and internal code */
68
- export interface Logger {
69
- error(message: string, context?: Record<string, unknown>): void;
70
- warn(message: string, context?: Record<string, unknown>): void;
71
- debug(message: string, context?: Record<string, unknown>): void;
72
- }
73
-
74
- /**
75
- * Centralized logger for omp.
76
- *
77
- * Logs to ~/.omp/logs/omp.YYYY-MM-DD.log with size-based rotation.
78
- * Safe for concurrent access from multiple omp instances.
79
- *
80
- * @example
81
- * ```typescript
82
- * import { logger } from "../core/logger";
83
- *
84
- * logger.error("MCP request failed", { url, method });
85
- * logger.warn("Theme file invalid, using fallback", { path });
86
- * logger.debug("LSP fallback triggered", { reason });
87
- * ```
88
- */
89
- export const logger: Logger = {
90
- error(message: string, context?: Record<string, unknown>): void {
91
- try {
92
- winstonLogger.error(message, context);
93
- } catch {
94
- // Silently ignore logging failures
95
- }
96
- },
97
- warn(message: string, context?: Record<string, unknown>): void {
98
- try {
99
- winstonLogger.warn(message, context);
100
- } catch {
101
- // Silently ignore logging failures
102
- }
103
- },
104
- debug(message: string, context?: Record<string, unknown>): void {
105
- try {
106
- winstonLogger.debug(message, context);
107
- } catch {
108
- // Silently ignore logging failures
109
- }
110
- },
111
- };
@@ -1,23 +0,0 @@
1
- /**
2
- * Async cleanup registry for graceful shutdown on signals.
3
- */
4
-
5
- /** Registry of async cleanup callbacks to run on shutdown/signals */
6
- const asyncCleanupCallbacks: (() => Promise<void>)[] = [];
7
-
8
- /**
9
- * Register an async cleanup callback to be run on process signals (SIGINT, SIGTERM, SIGHUP).
10
- * Returns an unsubscribe function.
11
- */
12
- export function registerAsyncCleanup(callback: () => Promise<void>): () => void {
13
- asyncCleanupCallbacks.push(callback);
14
- return () => {
15
- const index = asyncCleanupCallbacks.indexOf(callback);
16
- if (index >= 0) asyncCleanupCallbacks.splice(index, 1);
17
- };
18
- }
19
-
20
- /** Run all registered async cleanup callbacks, settling all promises */
21
- export async function runAsyncCleanup(): Promise<void> {
22
- await Promise.allSettled(asyncCleanupCallbacks.map((cb) => cb()));
23
- }