@oh-my-pi/pi-coding-agent 6.7.67 → 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.
- package/CHANGELOG.md +28 -0
- package/package.json +6 -7
- package/src/cli/session-picker.ts +27 -28
- package/src/cli/setup-cli.ts +7 -16
- package/src/cli/update-cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/core/agent-session.ts +202 -37
- package/src/core/agent-storage.ts +1 -1
- package/src/core/auth-storage.ts +15 -25
- package/src/core/bash-executor.ts +63 -105
- package/src/core/custom-commands/loader.ts +1 -1
- package/src/core/custom-tools/loader.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -2
- package/src/core/exec.ts +16 -100
- package/src/core/extensions/index.ts +1 -7
- package/src/core/extensions/loader.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +2 -2
- package/src/core/extensions/wrapper.ts +15 -20
- package/src/core/frontmatter.ts +1 -1
- package/src/core/history-storage.ts +3 -6
- package/src/core/hooks/index.ts +2 -2
- package/src/core/hooks/loader.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +14 -26
- package/src/core/hooks/types.ts +1 -2
- package/src/core/keybindings.ts +1 -1
- package/src/core/mcp/client.ts +13 -13
- package/src/core/mcp/json-rpc.ts +1 -1
- package/src/core/mcp/loader.ts +1 -1
- package/src/core/mcp/manager.ts +2 -2
- package/src/core/mcp/tool-cache.ts +1 -1
- package/src/core/mcp/transports/http.ts +32 -70
- package/src/core/model-registry.ts +1 -1
- package/src/core/plugins/installer.ts +13 -11
- package/src/core/prompt-templates.ts +4 -9
- package/src/core/python-executor.ts +23 -18
- package/src/core/python-gateway-coordinator.ts +29 -28
- package/src/core/python-kernel.ts +230 -211
- package/src/core/sdk.ts +10 -13
- package/src/core/session-manager.ts +1 -1
- package/src/core/settings-manager.ts +22 -9
- package/src/core/skills.ts +1 -1
- package/src/core/ssh/connection-manager.ts +19 -33
- package/src/core/ssh/ssh-executor.ts +39 -35
- package/src/core/ssh/sshfs-mount.ts +14 -33
- package/src/core/storage-migration.ts +1 -1
- package/src/core/streaming-output.ts +183 -127
- package/src/core/system-prompt.ts +119 -79
- package/src/core/title-generator.ts +1 -1
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +3 -3
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +1 -1
- package/src/core/tools/exa/render.ts +1 -1
- package/src/core/tools/find.ts +39 -71
- package/src/core/tools/gemini-image.ts +1 -1
- package/src/core/tools/grep.ts +88 -100
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/ls.ts +1 -1
- package/src/core/tools/lsp/client.ts +50 -50
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/index.ts +2 -4
- package/src/core/tools/lsp/lspmux.ts +1 -1
- package/src/core/tools/lsp/rust-analyzer.ts +2 -2
- package/src/core/tools/lsp/utils.ts +0 -14
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/patch/shared.ts +3 -4
- package/src/core/tools/python.ts +3 -3
- package/src/core/tools/read.ts +29 -68
- package/src/core/tools/render-utils.ts +0 -5
- package/src/core/tools/ssh.ts +3 -3
- package/src/core/tools/task/model-resolver.ts +7 -9
- package/src/core/tools/task/worker.ts +144 -139
- package/src/core/tools/todo-write.ts +1 -1
- package/src/core/tools/truncate.ts +2 -2
- package/src/core/tools/web-fetch.ts +13 -15
- package/src/core/tools/web-scrapers/types.ts +1 -3
- package/src/core/tools/web-scrapers/utils.ts +14 -13
- package/src/core/tools/web-scrapers/youtube.ts +39 -12
- package/src/core/tools/web-search/auth.ts +9 -45
- package/src/core/tools/write.ts +1 -1
- package/src/core/ttsr.ts +1 -1
- package/src/core/utils.ts +1 -187
- package/src/core/voice-controller.ts +1 -1
- package/src/core/voice-supervisor.ts +11 -38
- package/src/core/voice.ts +1 -8
- package/src/discovery/codex.ts +1 -1
- package/src/index.ts +4 -4
- package/src/main.ts +5 -10
- package/src/migrations.ts +1 -1
- package/src/modes/index.ts +7 -40
- package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
- package/src/modes/interactive/components/hook-editor.ts +12 -9
- package/src/modes/interactive/components/login-dialog.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line.ts +36 -35
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +1 -1
- package/src/modes/interactive/controllers/command-controller.ts +50 -84
- package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
- package/src/modes/interactive/controllers/input-controller.ts +12 -11
- package/src/modes/interactive/interactive-mode.ts +10 -11
- package/src/modes/interactive/theme/theme.ts +1 -1
- package/src/modes/interactive/types.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +91 -121
- package/src/modes/rpc/rpc-mode.ts +71 -79
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/utils/clipboard.ts +57 -141
- package/src/utils/shell-snapshot.ts +12 -60
- package/src/utils/shell.ts +35 -56
- package/src/utils/tools-manager.ts +42 -71
- package/src/core/logger.ts +0 -111
- package/src/modes/cleanup.ts +0 -23
package/src/utils/shell.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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:
|
|
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():
|
|
190
|
-
if (pgrepAvailable ===
|
|
185
|
+
function hasPgrep(): string | null {
|
|
186
|
+
if (pgrepAvailable === undefined) {
|
|
191
187
|
try {
|
|
192
|
-
|
|
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 =
|
|
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 =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
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 (
|
|
152
|
+
if (await Bun.file(localPath).exists()) {
|
|
157
153
|
return localPath;
|
|
158
154
|
}
|
|
159
155
|
|
|
160
156
|
// Check system PATH
|
|
161
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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(
|
|
241
|
+
extractedBinary = join(tmp.path, config.binaryName + binaryExt);
|
|
265
242
|
} else {
|
|
266
|
-
const extractedDir = join(
|
|
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 (
|
|
271
|
-
|
|
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
|
-
|
|
255
|
+
await chmod(binaryPath, 0o755);
|
|
279
256
|
}
|
|
280
257
|
} finally {
|
|
281
258
|
// Cleanup
|
|
282
|
-
|
|
283
|
-
|
|
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 =
|
|
269
|
+
const uv = Bun.which("uv");
|
|
293
270
|
if (uv) {
|
|
294
|
-
const result =
|
|
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 =
|
|
276
|
+
const pip = Bun.which("pip3") || Bun.which("pip");
|
|
304
277
|
if (pip) {
|
|
305
|
-
const result =
|
|
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
|
-
|
|
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 =
|
|
302
|
+
const path = Bun.which(pythonConfig.binaryName);
|
|
334
303
|
if (path) {
|
|
335
304
|
if (!silent) {
|
|
336
|
-
|
|
305
|
+
logger.debug(`${pythonConfig.name} installed successfully`);
|
|
337
306
|
}
|
|
338
307
|
return path;
|
|
339
308
|
}
|
|
340
309
|
}
|
|
341
310
|
if (!silent) {
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
+
logger.debug(`${config.name} installed to ${path}`);
|
|
359
328
|
}
|
|
360
329
|
return path;
|
|
361
330
|
} catch (e) {
|
|
362
331
|
if (!silent) {
|
|
363
|
-
|
|
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
|
}
|
package/src/core/logger.ts
DELETED
|
@@ -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
|
-
};
|
package/src/modes/cleanup.ts
DELETED
|
@@ -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
|
-
}
|