refactor(fs): migrated remaining sync file operations to async

- Converted readdirSync, readFileSync, and statSync to async readdir, readFile, stat across skills and agent discovery.
- Made scanDirectoryForSkills async and refactored custom directory scanning to use Promise.all for concurrent processing.
- Updated agent discovery to use fs/promises for async file reading and refactored helper patterns.
- Added AgentParsingError exception class for better error handling during agent parsing.
- Added filesystem error type guards (isEnoent, isEacces, isPerm, etc.) to pi-utils for safe error checking.
- Added color manipulation utilities to pi-utils for accessibility features.
- Added color-blind mode setting to settings manager.
- Migrated plugins, settings, and config modules from sync to async file operations.
- Updated error handling to use new pi-utils type guards for type-safe checking.
This commit is contained in:
can1357
2026-01-24 00:49:24 +01:00
parent f66e5dba9b
commit e22d123009
190 changed files with 3800 additions and 2885 deletions
+161 -18
View File
@@ -8,14 +8,14 @@ This repo contains multiple packages, but **`packages/coding-agent/`** is the pr
### Package Structure
| Package | Description |
|---------|-------------|
| `packages/ai` | Multi-provider LLM client with streaming support |
| `packages/agent` | Agent runtime with tool calling and state management |
| `packages/coding-agent` | Main CLI application (primary focus) |
| `packages/tui` | Terminal UI library with differential rendering |
| `packages/stats` | Local observability dashboard (`omp stats`) |
| `packages/pi-utils` | Shared utilities (logger, streams, temp files) |
| Package | Description |
| ----------------------- | ---------------------------------------------------- |
| `packages/ai` | Multi-provider LLM client with streaming support |
| `packages/agent` | Agent runtime with tool calling and state management |
| `packages/coding-agent` | Main CLI application (primary focus) |
| `packages/tui` | Terminal UI library with differential rendering |
| `packages/stats` | Local observability dashboard (`omp stats`) |
| `packages/pi-utils` | Shared utilities (logger, streams, temp files) |
## Code Quality
@@ -37,25 +37,28 @@ This project uses Bun. Use Bun APIs where they provide a cleaner alternative; us
### Process Execution
**Prefer Bun Shell** (`$` template literals) for simple commands:
```typescript
import { $ } from "bun";
// Capture output
const result = await $`git status`.cwd(dir).quiet().nothrow();
if (result.exitCode === 0) {
const text = result.text();
const text = result.text();
}
// Fire and forget
$`rm ${tmpFile}`.quiet().nothrow();
$`do-stuff ${tmpFile}`.quiet().nothrow();
```
**Use `Bun.spawn`/`Bun.spawnSync`** only when:
- Long-running processes (LSP servers, Python kernels)
- Streaming stdin/stdout/stderr required (SSE, JSON-RPC)
- Process control needed (signals, kill, complex lifecycle)
**Bun Shell methods:**
- `.quiet()` - suppress output (stdout/stderr to null)
- `.nothrow()` - don't throw on non-zero exit
- `.text()` - get stdout as string
@@ -66,9 +69,43 @@ $`rm ${tmpFile}`.quiet().nothrow();
**Prefer** `await Bun.sleep(ms)`
**Avoid** `new Promise((resolve) => setTimeout(resolve, ms))`
### Node Module Imports
**NEVER use named imports from `node:fs` or `node:path`** — always use namespace imports:
```typescript
// BAD: Named imports
import { readdir, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
// GOOD: Namespace imports
import * as fs from "node:fs/promises";
import * as path from "node:path";
// Then use: fs.readdir(), path.join(), etc.
```
**Choosing between `node:fs` and `node:fs/promises`:**
- **Async-only file** → `import * as fs from "node:fs/promises"`
- **Needs both sync and async** → `import * as fs from "node:fs"`, use `fs.promises.xxx` for async
```typescript
// File with only async operations
import * as fs from "node:fs/promises";
await fs.readdir(dir);
await fs.stat(path);
// File mixing sync and async (e.g., sync in constructor, async in methods)
import * as fs from "node:fs";
fs.existsSync(path); // sync
await fs.promises.readdir(dir); // async
```
### File I/O
**Prefer Bun file APIs:**
```typescript
// Read
const text = await Bun.file(path).text();
@@ -79,22 +116,129 @@ const exists = await Bun.file(path).exists();
await Bun.write(path, data);
```
**Use `node:fs/promises`** for directories (Bun has no native directory APIs):
```typescript
import { mkdir, rm, readdir } from "node:fs/promises";
**`Bun.write()` is smart** — it auto-creates parent directories and uses optimal syscalls:
await mkdir(path, { recursive: true });
await rm(path, { recursive: true, force: true });
const entries = await readdir(path);
```typescript
// BAD: Redundant mkdir before write
await mkdir(dirname(path), { recursive: true });
await Bun.write(path, data);
// GOOD: Bun.write handles it
await Bun.write(path, data); // Creates parent dirs automatically
```
**Use `node:fs/promises`** for directories (Bun has no native directory APIs):
```typescript
import * as fs from "node:fs/promises";
await fs.mkdir(path, { recursive: true });
await fs.rm(path, { recursive: true, force: true });
const entries = await fs.readdir(path);
```
**Avoid sync APIs** in async flows:
- Don't use `existsSync`/`readFileSync`/`writeFileSync` when async is possible
- Use sync only when required by a synchronous interface
### File I/O Anti-Patterns
**NEVER check `.exists()` before reading** — use try-catch with error code:
```typescript
// BAD: Two syscalls, race condition
if (await Bun.file(path).exists()) {
return await Bun.file(path).json();
}
// BAD: Even with handle reuse, still two syscalls
const file = Bun.file(path);
if (await file.exists()) {
return await file.json();
}
// GOOD: One syscall, atomic, type-safe error handling
import { isEnoent } from "@anthropic/pi-utils";
try {
return await Bun.file(path).json();
} catch (err) {
if (isEnoent(err)) return null;
throw err;
}
```
**NEVER use `Bun.file().exists()` for directories** — it doesn't distinguish files from dirs:
```typescript
// BAD: Bun.file is for files, not directories
if (await Bun.file(dirPath).exists()) { ... }
// GOOD: Use fs.stat for directories
import * as fs from "node:fs/promises";
try {
const s = await fs.stat(dirPath);
if (s.isDirectory()) { ... }
} catch (err) {
if (isEnoent(err)) { /* doesn't exist */ }
}
```
**NEVER create multiple handles to the same path**:
```typescript
// BAD: Creates two file handles
if (await Bun.file(path).exists()) {
const content = await Bun.file(path).text();
}
// BAD: Still wasteful even in separate functions
async function checkConfig() {
return await Bun.file(configPath).exists();
}
async function loadConfig() {
return await Bun.file(configPath).json(); // second handle
}
```
**NEVER use `Buffer.from(await Bun.file(x).arrayBuffer())`** — just use `readFile`:
```typescript
// BAD: Unnecessary conversion
const buffer = Buffer.from(await Bun.file(path).arrayBuffer());
// GOOD: Direct buffer read
import * as fs from "node:fs/promises";
const buffer = await fs.readFile(path);
```
**NEVER mix redundant existence checks with try-catch**:
```typescript
// BAD: Existence check is pointless when you have try-catch
if (await file.exists()) {
try {
return await file.json();
} catch {
return null;
}
}
// GOOD: Let try-catch handle missing files
try {
return await Bun.file(path).json();
} catch (err) {
if (isEnoent(err)) return null;
throw err;
}
```
### Streams
**Prefer centralized helpers:**
```typescript
import { readStream, readLines } from "./utils/stream";
@@ -103,7 +247,7 @@ const text = await readStream(child.stdout);
// Line-by-line iteration
for await (const line of readLines(stream)) {
// process line
// process line
}
```
@@ -114,7 +258,6 @@ for await (const line of readLines(stream)) {
| Operation | Use | Not |
| --------------- | ------------------------------------- | ------------------------------- |
| File read/write | `Bun.file()`, `Bun.write()` | `readFileSync`, `writeFileSync` |
| File exists | `await Bun.file(path).exists()` | `existsSync` |
| Spawn process | `$\`cmd\``, `Bun.spawn()` | `child_process` |
| Sleep | `Bun.sleep(ms)` | `setTimeout` promise |
| Binary lookup | `Bun.which("git")` | `spawnSync(["which", "git"])` |
+145 -93
View File
@@ -9,15 +9,16 @@
"get-east-asian-width": "^1.4.0",
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@biomejs/biome": "^2.3.12",
"@bufbuild/protoc-gen-es": "^2.10.2",
"@types/bun": "^1.2.17",
"@types/node": "^22.10.5",
"@typescript/native-preview": "^7.0.0-dev.20260102.1",
"@types/node": "^25.0.10",
"@typescript/native-preview": "^7.0.0-dev.20260123.3",
"bun-types": "^1.3.5",
"concurrently": "^9.2.1",
"lint-staged": "^16.2.7",
"prettier": "^3.8.0",
"ts-morph": "^27.0.2",
},
},
"packages/agent": {
@@ -30,7 +31,7 @@
},
"devDependencies": {
"@sinclair/typebox": "^0.34.46",
"@types/node": "^24.3.0",
"@types/node": "^25.0.10",
},
},
"packages/ai": {
@@ -40,32 +41,32 @@
"pi-ai": "./src/cli.ts",
},
"dependencies": {
"@anthropic-ai/sdk": "0.71.2",
"@aws-sdk/client-bedrock-runtime": "^3.968.0",
"@anthropic-ai/sdk": "^0.71.2",
"@aws-sdk/client-bedrock-runtime": "^3.975.0",
"@bufbuild/protobuf": "^2.10.2",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-node": "^2.1.1",
"@google/genai": "1.34.0",
"@mistralai/mistralai": "1.10.0",
"@google/genai": "^1.38.0",
"@mistralai/mistralai": "^1.13.0",
"@oh-my-pi/pi-utils": "workspace:*",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"chalk": "^5.6.2",
"json5": "^2.2.3",
"openai": "6.10.0",
"openai": "^6.16.0",
"partial-json": "^0.1.7",
"zod-to-json-schema": "^3.24.6",
},
"devDependencies": {
"@types/node": "^24.3.0",
"@types/node": "^25.0.10",
},
},
"packages/coding-agent": {
"name": "@oh-my-pi/pi-coding-agent",
"version": "8.1.0",
"bin": {
"omp": "dist/cli.js",
"omp": "src/cli.ts",
},
"dependencies": {
"@oh-my-pi/omp-stats": "workspace:*",
@@ -73,29 +74,29 @@
"@oh-my-pi/pi-ai": "workspace:*",
"@oh-my-pi/pi-tui": "workspace:*",
"@oh-my-pi/pi-utils": "workspace:*",
"@openai/agents": "^0.3.7",
"@openai/agents": "^0.4.3",
"@sinclair/typebox": "^0.34.46",
"ajv": "^8.17.1",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",
"file-type": "^21.1.1",
"glob": "^11.0.3",
"glob": "^13.0.0",
"handlebars": "^4.7.8",
"highlight.js": "^11.11.1",
"marked": "^15.0.12",
"marked": "^17.0.1",
"nanoid": "^5.1.6",
"ndjson": "^2.0.0",
"node-html-parser": "^6.1.13",
"node-html-parser": "^7.0.2",
"smol-toml": "^1.6.0",
"strip-ansi": "^7.1.2",
"zod": "^4.3.5",
},
"devDependencies": {
"@types/diff": "^7.0.2",
"@types/diff": "^8.0.0",
"@types/ms": "^2.1.0",
"@types/ndjson": "^2.0.4",
"@types/node": "^24.3.0",
"@types/node": "^25.0.10",
"ms": "^2.1.3",
},
},
@@ -108,7 +109,7 @@
"winston-daily-rotate-file": "^5.0.0",
},
"devDependencies": {
"@types/node": "^24.3.0",
"@types/node": "^25.0.10",
},
},
"packages/react-edit-benchmark": {
@@ -126,18 +127,19 @@
},
"devDependencies": {
"@types/diff": "^7.0.2",
"bun-types": "^1.2.19",
},
},
"packages/stats": {
"name": "@oh-my-pi/omp-stats",
"version": "8.1.0",
"bin": {
"omp-stats": "./dist/index.js",
"omp-stats": "./src/index.ts",
},
"dependencies": {
"@oh-my-pi/pi-ai": "workspace:*",
"date-fns": "^4.1.0",
"lucide-react": "^0.562.0",
"lucide-react": "^0.563.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"recharts": "^3.6.0",
@@ -152,15 +154,15 @@
"name": "@oh-my-pi/pi-tui",
"version": "8.1.0",
"dependencies": {
"@types/mime-types": "^2.1.4",
"@types/mime-types": "^3.0.1",
"chalk": "^5.5.0",
"get-east-asian-width": "^1.3.0",
"marked": "^15.0.12",
"marked": "^17.0.1",
"mime-types": "^3.0.1",
},
"devDependencies": {
"@xterm/headless": "^5.5.0",
"@xterm/xterm": "^5.5.0",
"@xterm/headless": "^6.0.0",
"@xterm/xterm": "^6.0.0",
},
},
},
@@ -177,11 +179,11 @@
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/credential-provider-node": "^3.972.1", "@aws-sdk/eventstream-handler-node": "^3.972.1", "@aws-sdk/middleware-eventstream": "^3.972.1", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/middleware-websocket": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/token-providers": "3.974.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-x/xKuWcQ6tJaaJedkAtoLB5IqAn4Zblq3j5OS1PCNUjhKDVfiSV1ZOuD3/cAWRUDPROiZvNK1POGLkBtin3cxg=="],
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.975.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.1", "@aws-sdk/credential-provider-node": "^3.972.1", "@aws-sdk/eventstream-handler-node": "^3.972.1", "@aws-sdk/middleware-eventstream": "^3.972.1", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.2", "@aws-sdk/middleware-websocket": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/token-providers": "3.975.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.1", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.11", "@smithy/middleware-retry": "^4.4.27", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.26", "@smithy/util-defaults-mode-node": "^4.2.29", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZptHL8Z8y2m6sq1ksl+MIGoXxzRkWuOzqbGOd+P5htwIX0kEvzmxPwAqyCoiULn1OjS+kB+TCxfvBUVyglq3MQ=="],
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ci+GiM0c4ULo4D79UMcY06LcOLcfvUfiyt8PzNY0vbt5O8BfCPYf4QomwVgkNcLLCYmroO4ge2Yy1EsLUlcD6g=="],
"@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/core": ["@aws-sdk/core@3.973.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ocubx42QsMyVs9ANSmFpRm0S+hubWljpPLjOi9UFrtcnVJjrVJTzQ51sN0e5g4e8i8QZ7uY73zosLmgYL7kZTQ=="],
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-/etNHqnx96phy/SjI0HRC588o4vKH5F0xfkZ13yAATV7aNrb+5gYGNE6ePWafP+FuZ3HkULSSlJFj0AxgrAqYw=="],
@@ -209,15 +211,15 @@
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.2", "", { "dependencies": { "@aws-sdk/core": "^3.973.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-d+Exq074wy0X6wvShg/kmZVtkah+28vMuqCtuY3cydg8LUZOJBtbAolCpEJizSyb8mJJZF9BjWaTANXL4OYnkg=="],
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-format-url": "^3.972.1", "@smithy/eventstream-codec": "^4.2.8", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zej/2+u6KCEHUipHW/4KXwj+4PTJkrORwR4KHNHE8ATdzLf7hwu7HsPK+TyQXZSftH+VqYkFmTbyF9OPxOpwmw=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="],
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.975.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.1", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.2", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.11", "@smithy/middleware-retry": "^4.4.27", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.26", "@smithy/util-defaults-mode-node": "^4.2.29", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-OkeFHPlQj2c/Y5bQGkX14pxhDWUGUFt3LRHhjcDKsSCw6lrxKcxN3WFZN0qbJwKNydP+knL5nxvfgKiCLpTLRA=="],
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.974.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ=="],
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.975.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.1", "@aws-sdk/nested-clients": "3.975.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-AWQt64hkVbDQ+CmM09wnvSk2mVyH4iRROkmYkr3/lmUtFNbE2L/fnw26sckZnUcFCsHPqbkQrcsZAnTcBLbH4w=="],
"@aws-sdk/types": ["@aws-sdk/types@3.973.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ=="],
@@ -237,23 +239,23 @@
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.11", "@biomejs/cli-darwin-x64": "2.3.11", "@biomejs/cli-linux-arm64": "2.3.11", "@biomejs/cli-linux-arm64-musl": "2.3.11", "@biomejs/cli-linux-x64": "2.3.11", "@biomejs/cli-linux-x64-musl": "2.3.11", "@biomejs/cli-win32-arm64": "2.3.11", "@biomejs/cli-win32-x64": "2.3.11" }, "bin": { "biome": "bin/biome" } }, "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ=="],
"@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="],
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
@@ -271,7 +273,7 @@
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
"@google/genai": ["@google/genai@1.34.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.24.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw=="],
"@google/genai": ["@google/genai@1.38.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-V/4CQVQGovvGHuS73lwJwHKR9x33kCij3zz/ReEQ4A7RJaV0U7m4k1mvYhFk55cGZdF5JLKu2S9BTaFuEs5xTA=="],
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
@@ -281,7 +283,7 @@
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="],
"@mistralai/mistralai": ["@mistralai/mistralai@1.13.0", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-eg7/8hQp91SshYDJ5XdWDsJENdIaSu52npJfKLCPuhla8Vm6GlESv2RROQyxESglCl7b1OULWiAv1ERbsrZO7w=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="],
@@ -299,16 +301,36 @@
"@oh-my-pi/react-edit-benchmark": ["@oh-my-pi/react-edit-benchmark@workspace:packages/react-edit-benchmark"],
"@openai/agents": ["@openai/agents@0.3.9", "", { "dependencies": { "@openai/agents-core": "0.3.9", "@openai/agents-openai": "0.3.9", "@openai/agents-realtime": "0.3.9", "debug": "^4.4.0", "openai": "^6" }, "peerDependencies": { "zod": "^3.25.40 || ^4.0" } }, "sha512-YaKnqv0M6bCVvn47pThkFfyHz8xWJ+0Ll9ZnhvwJZ5gyPX0UxHIUeUs9SMG9BSvNuJNJHlc5uvfUDGYAmKJClw=="],
"@openai/agents": ["@openai/agents@0.4.3", "", { "dependencies": { "@openai/agents-core": "0.4.3", "@openai/agents-openai": "0.4.3", "@openai/agents-realtime": "0.4.3", "debug": "^4.4.0", "openai": "^6" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-vj1vRkmhex4r+aQ3EjI9s9u4veFx/kT6NsZmj5qEvBTn8tk7HxPjZJR601dIVBISspH+dqh276I9e2thydPc3w=="],
"@openai/agents-core": ["@openai/agents-core@0.3.9", "", { "dependencies": { "debug": "^4.4.0", "openai": "^6" }, "optionalDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "peerDependencies": { "zod": "^3.25.40 || ^4.0" }, "optionalPeers": ["zod"] }, "sha512-6Fr/VkA3lMaTT9EV2+OsmkMX9Yx+/PeWtlmaWNKDRG8D15IWuK13NOC9eFklTsa7otbuwbw/Xmjes+h4Z+CwSQ=="],
"@openai/agents-core": ["@openai/agents-core@0.4.3", "", { "dependencies": { "debug": "^4.4.0", "openai": "^6" }, "optionalDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-2cz7ptgdHp2NzoN9uImBypRmR+e8ExvBXhxdDnNE6Cw0R8LQxd/KfdDupg7BB1inleOKLgDoasBXD7jRZ0iSQA=="],
"@openai/agents-openai": ["@openai/agents-openai@0.3.9", "", { "dependencies": { "@openai/agents-core": "0.3.9", "debug": "^4.4.0", "openai": "^6" }, "peerDependencies": { "zod": "^3.25.40 || ^4.0" } }, "sha512-duXUt0xU6K/+c7ae4m8BrJIUzZal6Pzln8V0frnJfNyfYO4SvHMV4qwPRzVDvv/ANj4DQXWI2L1JdPxKJeSHkw=="],
"@openai/agents-openai": ["@openai/agents-openai@0.4.3", "", { "dependencies": { "@openai/agents-core": "0.4.3", "debug": "^4.4.0", "openai": "^6" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-dBQDr6IJR9+h/7+0Xn0CQhBbVWvn3PE3a56WuwbKr3cYHU9QxUbVrsRprx3ERboww2qy2D9Hk6Cik2q9MHDnZg=="],
"@openai/agents-realtime": ["@openai/agents-realtime@0.3.9", "", { "dependencies": { "@openai/agents-core": "0.3.9", "@types/ws": "^8.18.1", "debug": "^4.4.0", "ws": "^8.18.1" }, "peerDependencies": { "zod": "^3.25.40 || ^4.0" } }, "sha512-51zHO/zao/LHv70gseU1otTvXyS81tuVaewHlUBiNMXvqSZNkYViiO69hpXMoTYn5c3gCjUrXPxxI+NlHUtaHg=="],
"@openai/agents-realtime": ["@openai/agents-realtime@0.4.3", "", { "dependencies": { "@openai/agents-core": "0.4.3", "@types/ws": "^8.18.1", "debug": "^4.4.0", "ws": "^8.18.1" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-hfe69MfEM0KRN/auLzca7UlMl8mcNMhnIQfOb01R0SbFpU1yl332kQEZw3a2o+n9YRCVAmP6QKuJjejsZwqbvg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
@@ -413,6 +435,8 @@
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="],
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -433,15 +457,15 @@
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/diff": ["@types/diff@7.0.2", "", {}, "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q=="],
"@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="],
"@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="],
"@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/ndjson": ["@types/ndjson@2.0.4", "", { "dependencies": { "@types/node": "*", "@types/through": "*" } }, "sha512-ajAl7AjhFstF6waORYNSS49GL5iBKisqJlgvXuprXFKCX9fto4ordlNU3+XMgkMddgeR0WoQQBmKUk0v0dJ4pw=="],
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
@@ -455,27 +479,27 @@
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260122.4", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260122.4", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260122.4", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260122.4", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260122.4", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260122.4", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260122.4", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260122.4" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-lboRukXxL3jeIyMKc4EjNHI4QThjOrrT5jjCyaNOUFkrk3JiObrR4z7Z6Z+m+WM/gbSW2tqaRBRFBRMsZGsxKQ=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260123.3", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260123.3", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260123.3", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260123.3", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260123.3", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260123.3", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260123.3", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260123.3" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-VE8ebP25U8B3XNIe1azQka7ty+lgYse7j8UtKuEbbC2G8JL/QY2iGIFaeR936h6ezPIjSDsHVNlsV5C5VQY5qA=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260122.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8hIIPe6LoY+FmUBRvhX7IbsZCr4Puwps0Ok+ez+rzg7d+sJCVuJ37ZxFh1pcNJEM39eII2o5TZ9dTCIT7/aAWA=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260123.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sIVxtOi5KXcbxyA7zMkB0zw4BQX5muehXciQlGNBuAqupQJgxbUPNuypXcmJKFDDUdEkEARRS/whtG0SA3HTrg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260122.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-kJNpVzPW9jcFuRco+QwSHToEFHmfovNmwLo9PY97DLMqGnrvxkSy+k5sClHscEThIdzSRQbi7uZJUVBwgohNmA=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260123.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1yF90TxZvMa7MmlP7ExifqLOvPl4wNsTB58rE1hK9b6BAB/7/nx+CeYuWIoID4CAG2r1TY1EPqpJkbFyd6w2Rw=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260122.4", "", { "os": "linux", "cpu": "arm" }, "sha512-ImSMhsrB9QMK/cT2s4yCWkWOBUGZ+1L6vBaMMdyw3eNrU4tQo85Z3AqTn12M8WRbefLL89OmP6zJppP2TH3PAQ=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260123.3", "", { "os": "linux", "cpu": "arm" }, "sha512-H8y6qC4mJhzJvtq2bXfNcoD86KOIB7bCa571Mv7r5cATzN4BVG7pym6m49N+m5Tnoe1BkazWbMUZlH6yxrqiUQ=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260122.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-QsSNK3PbRyNviv0xru21jGV8fgzTVJ26oMnQwoK0IcyGabXtcLHJe5I5y4lwvLNjZPCbKDNThawso54Fo72FJQ=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260123.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-GiiukbZlzxrigq+52NemzgAleQ2iM9k88KGZnQgy8Y/7Rf73B8YgBrkGipgUZCcAWbvlhDooXMLAuQLXc6wdwQ=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260122.4", "", { "os": "linux", "cpu": "x64" }, "sha512-0OyzmbZ2Zq039RPLCyEjcrBhDkCXDfIrwbMJfoOrCyEO9XrK9Icwqzogm7H4bLyhcfuZIgugYSP2mQflaDvOVg=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260123.3", "", { "os": "linux", "cpu": "x64" }, "sha512-CsP6Aw7b3XNd293VYBfl6K1PYVLMjxYcyBRUpu9JX8l9X4RbTdJl6O1/Ab5/X6l+C8ukv2w0lR3CavX2K1kxnQ=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260122.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-FSRZ0iylGKbrYiimfkqKBXRlhR+PwOz5Jcb57dAU7wzr/7xSGON9bfywb0BhoF31GjYAnZ5Xv+1fuQOiB2HBYw=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260123.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Za3ra9JhTRUakBuxCIa5qJnwlkHUgMjBZ9p/BH3lQp3Wwx0dhav3aA+9is514iSZw8fR/q4hhesFXCrJW8XQzg=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260122.4", "", { "os": "win32", "cpu": "x64" }, "sha512-vAzRnS4nMBAd2XduzdmrhsCAAVSbCzZxVGMIKVvRcL9ljO5+fooggiYq7sk798TIZ1ov7A0rZk5k+o0Wyx2nXA=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260123.3", "", { "os": "win32", "cpu": "x64" }, "sha512-H7/PE9vznvc4mQz6Hiuw4tTQq9J0b5QWebYoGXoxOqFF/JI3m920yv8+nRI144cVoBYdonOwanexFfNq4KdwCQ=="],
"@typescript/vfs": ["@typescript/vfs@1.6.2", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g=="],
"@xterm/headless": ["@xterm/headless@5.5.0", "", {}, "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g=="],
"@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="],
"@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
"@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -533,6 +557,8 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -657,6 +683,8 @@
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
@@ -693,7 +721,7 @@
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
@@ -743,7 +771,7 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
@@ -773,11 +801,13 @@
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -819,7 +849,7 @@
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-html-parser": ["node-html-parser@6.1.13", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg=="],
"node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
@@ -837,7 +867,7 @@
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"openai": ["openai@6.10.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A=="],
"openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
@@ -849,6 +879,8 @@
"partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
@@ -863,6 +895,8 @@
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
@@ -973,6 +1007,8 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
@@ -985,6 +1021,8 @@
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
"ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
@@ -995,7 +1033,7 @@
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -1043,23 +1081,43 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-sso/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/client-sso/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="],
"@aws-sdk/credential-provider-login/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-login/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="],
"@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.974.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/nested-clients": "3.974.0", "@aws-sdk/types": "^3.973.0", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ=="],
"@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="],
"@aws-sdk/util-endpoints/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="],
"@aws-sdk/util-user-agent-node/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"@mistralai/mistralai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@oh-my-pi/omp-stats/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
"@oh-my-pi/pi-agent-core/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
"@oh-my-pi/pi-ai/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
"@oh-my-pi/pi-coding-agent/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
"@oh-my-pi/pi-utils/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="],
"@oh-my-pi/react-edit-benchmark/@types/diff": ["@types/diff@7.0.2", "", {}, "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q=="],
"@reduxjs/toolkit/immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="],
@@ -1103,6 +1161,8 @@
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -1113,28 +1173,20 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"@aws-sdk/credential-provider-login/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.974.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.0", "@aws-sdk/middleware-host-header": "^3.972.1", "@aws-sdk/middleware-logger": "^3.972.1", "@aws-sdk/middleware-recursion-detection": "^3.972.1", "@aws-sdk/middleware-user-agent": "^3.972.1", "@aws-sdk/region-config-resolver": "^3.972.1", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@aws-sdk/util-user-agent-browser": "^3.972.1", "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.21.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.10", "@smithy/middleware-retry": "^4.4.26", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.25", "@smithy/util-defaults-mode-node": "^4.2.28", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q=="],
"@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"@aws-sdk/util-user-agent-node/@aws-sdk/middleware-user-agent/@aws-sdk/core": ["@aws-sdk/core@3.973.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.0", "@aws-sdk/xml-builder": "^3.972.1", "@smithy/core": "^3.21.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.10.11", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qy3Fmt8z4PRInM3ZqJmHihQ2tfCdj/MzbGaZpuHjYjgl1/Gcar4Pyp/zzHXh9hGEb61WNbWgsJcDUhnGIiX1TA=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@oh-my-pi/omp-stats/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@oh-my-pi/pi-agent-core/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@oh-my-pi/pi-ai/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@oh-my-pi/pi-coding-agent/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@oh-my-pi/pi-utils/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/ndjson/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/through/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
@@ -1147,8 +1199,6 @@
"concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
@@ -1165,6 +1215,8 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.1", "", { "dependencies": { "@aws-sdk/core": "^3.973.0", "@aws-sdk/types": "^3.973.0", "@aws-sdk/util-endpoints": "3.972.0", "@smithy/core": "^3.21.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6SVg4pY/9Oq9MLzO48xuM3lsOb8Rxg55qprEtFRpkUmuvKij31f5SQHEGxuiZ4RqIKrfjr2WMuIgXvqJ0eJsPA=="],
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+5 -4
View File
@@ -24,15 +24,16 @@
"release": "bun scripts/release.ts"
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@biomejs/biome": "^2.3.12",
"@bufbuild/protoc-gen-es": "^2.10.2",
"@types/bun": "^1.2.17",
"@types/node": "^22.10.5",
"@typescript/native-preview": "^7.0.0-dev.20260102.1",
"@types/node": "^25.0.10",
"@typescript/native-preview": "^7.0.0-dev.20260123.3",
"bun-types": "^1.3.5",
"concurrently": "^9.2.1",
"lint-staged": "^16.2.7",
"prettier": "^3.8.0"
"prettier": "^3.8.0",
"ts-morph": "^27.0.2"
},
"lint-staged": {
"*.{js,ts,jsx,tsx,json,jsonc,css}": "biome check --write --no-errors-on-unmatched"
+1 -1
View File
@@ -47,6 +47,6 @@
},
"devDependencies": {
"@sinclair/typebox": "^0.34.46",
"@types/node": "^24.3.0"
"@types/node": "^25.0.10"
}
}
+1 -4
View File
@@ -1,9 +1,6 @@
# Changelog
## [Unreleased]
### Fixed
- Fixed orphaned tool result handling by tracking skipped tool call IDs in message conversion to prevent dangling references
## [8.0.0] - 2026-01-23
### Fixed
@@ -119,7 +116,7 @@
- Replaced custom sleep implementations with Bun.sleep and abortableSleep
- Simplified SSE stream parsing using readLines utility
- Updated test framework from vitest to bun:test
- Replaced temp directory creation with createTempDirSync utility
- Replaced temp directory creation with TempDir API
- Changed credential storage from auth.json to ~/.omp/agent/agent.db
- Changed CLI command examples from npx to bunx
- Refactored OAuth flows to use common callback server base class
+2 -3
View File
@@ -551,10 +551,9 @@ The abort signal allows you to cancel in-progress requests. Aborted requests hav
import { getModel, stream } from "@oh-my-pi/pi-ai";
const model = getModel("openai", "gpt-4o-mini");
const controller = new AbortController();
// Abort after 2 seconds
setTimeout(() => controller.abort(), 2000);
const signal = AbortSignal.timeout(2000);
const s = stream(
model,
@@ -562,7 +561,7 @@ const s = stream(
messages: [{ role: "user", content: "Write a long story" }],
},
{
signal: controller.signal,
signal,
},
);
+6 -6
View File
@@ -57,19 +57,19 @@
},
"dependencies": {
"@oh-my-pi/pi-utils": "workspace:*",
"@anthropic-ai/sdk": "0.71.2",
"@aws-sdk/client-bedrock-runtime": "^3.968.0",
"@anthropic-ai/sdk": "^0.71.2",
"@aws-sdk/client-bedrock-runtime": "^3.975.0",
"@bufbuild/protobuf": "^2.10.2",
"@connectrpc/connect": "^2.1.1",
"@connectrpc/connect-node": "^2.1.1",
"@google/genai": "1.34.0",
"@mistralai/mistralai": "1.10.0",
"@google/genai": "^1.38.0",
"@mistralai/mistralai": "^1.13.0",
"@sinclair/typebox": "^0.34.41",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"chalk": "^5.6.2",
"json5": "^2.2.3",
"openai": "6.10.0",
"openai": "^6.16.0",
"partial-json": "^0.1.7",
"zod-to-json-schema": "^3.24.6"
},
@@ -93,6 +93,6 @@
"bun": ">=1.0.0"
},
"devDependencies": {
"@types/node": "^24.3.0"
"@types/node": "^25.0.10"
}
}
+3 -3
View File
@@ -23,7 +23,7 @@ async function login(provider: OAuthProvider): Promise<void> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
const promptFn = (msg: string) => prompt(rl, `${msg} `);
const storage = new CliAuthStorage();
const storage = await CliAuthStorage.create();
try {
let credentials: OAuthCredentials;
@@ -145,7 +145,7 @@ Examples:
}
if (command === "status") {
const storage = new CliAuthStorage();
const storage = await CliAuthStorage.create();
try {
const providers = storage.listProviders();
if (providers.length === 0) {
@@ -179,7 +179,7 @@ Examples:
if (command === "logout") {
let provider = args[1] as OAuthProvider | undefined;
const storage = new CliAuthStorage();
const storage = await CliAuthStorage.create();
try {
if (!provider) {
+2 -2
View File
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { appendFile } from "node:fs/promises";
import * as fs from "node:fs/promises";
import http2 from "node:http2";
import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
import { ValueSchema } from "@bufbuild/protobuf/wkt";
@@ -142,7 +142,7 @@ async function appendCursorDebugLog(entry: CursorLogEntry): Promise<void> {
const logPath = process.env.DEBUG_CURSOR_LOG;
if (!logPath) return;
try {
await appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
await fs.appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
} catch {
// Ignore debug log failures
}
+28 -18
View File
@@ -4,9 +4,9 @@
*/
import { Database } from "bun:sqlite";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import * as fs from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import * as path from "node:path";
import type { OAuthCredentials } from "./utils/oauth/types";
type AuthCredential = { type: "api_key"; key: string } | ({ type: "oauth" } & OAuthCredentials);
@@ -24,7 +24,7 @@ type AuthRow = {
* Get the agent config directory (e.g., ~/.omp/agent/)
*/
function getAgentDir(): string {
const configDir = process.env.OMP_CODING_AGENT_DIR || join(homedir(), ".omp", "agent");
const configDir = process.env.OMP_CODING_AGENT_DIR || path.join(homedir(), ".omp", "agent");
return configDir;
}
@@ -32,7 +32,7 @@ function getAgentDir(): string {
* Get path to agent.db
*/
function getAgentDbPath(): string {
return join(getAgentDir(), "agent.db");
return path.join(getAgentDir(), "agent.db");
}
function serializeCredential(credential: AuthCredential): { credentialType: string; data: string } | null {
@@ -79,6 +79,8 @@ function deserializeCredential(row: AuthRow): AuthCredential | null {
/**
* Simple storage class for CLI auth credentials.
*
* Use `CliAuthStorage.create()` to instantiate (async initialization).
*/
export class CliAuthStorage {
private db: Database;
@@ -87,20 +89,8 @@ export class CliAuthStorage {
private listAllStmt: ReturnType<Database["prepare"]>;
private deleteByProviderStmt: ReturnType<Database["prepare"]>;
constructor(dbPath: string = getAgentDbPath()) {
// Ensure directory exists with secure permissions
const dir = dirname(dbPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 0o700 });
}
this.db = new Database(dbPath);
// Harden database file permissions to prevent credential leakage
try {
chmodSync(dbPath, 0o600);
} catch {
// Ignore chmod failures (e.g., Windows)
}
private constructor(db: Database) {
this.db = db;
this.initializeSchema();
this.insertStmt = this.db.prepare(
@@ -111,6 +101,26 @@ export class CliAuthStorage {
this.deleteByProviderStmt = this.db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
}
static async create(dbPath: string = getAgentDbPath()): Promise<CliAuthStorage> {
const dir = path.dirname(dbPath);
const dirExists = await fs
.stat(dir)
.then((s) => s.isDirectory())
.catch(() => false);
if (!dirExists) {
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
}
const db = new Database(dbPath);
try {
await fs.chmod(dbPath, 0o600);
} catch {
// Ignore chmod failures (e.g., Windows)
}
return new CliAuthStorage(db);
}
private initializeSchema(): void {
this.db.exec(`
PRAGMA journal_mode=WAL;
+5 -5
View File
@@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import * as fs from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import * as path from "node:path";
import { supportsXhigh } from "./models";
import { type BedrockOptions, streamBedrock } from "./providers/amazon-bedrock";
import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic";
@@ -34,10 +34,10 @@ function hasVertexAdcCredentials(): boolean {
if (cachedVertexAdcCredentialsExists === null) {
const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
if (gacPath) {
cachedVertexAdcCredentialsExists = existsSync(gacPath);
cachedVertexAdcCredentialsExists = fs.existsSync(gacPath);
} else {
cachedVertexAdcCredentialsExists = existsSync(
join(homedir(), ".config", "gcloud", "application_default_credentials.json"),
cachedVertexAdcCredentialsExists = fs.existsSync(
path.join(homedir(), ".config", "gcloud", "application_default_credentials.json"),
);
}
}
+42 -42
View File
@@ -69,30 +69,30 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { execSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { getModel } from "@oh-my-pi/pi-ai/models";
import { complete } from "@oh-my-pi/pi-ai/stream";
import type { Api, Context, ImageContent, Model, OptionsForApi, UserMessage } from "@oh-my-pi/pi-ai/types";
import { e2eApiKey } from "./oauth";
const TEMP_DIR = join(import.meta.dir, ".temp-images");
const TEMP_DIR = path.join(import.meta.dir, ".temp-images");
/**
* Generate a valid PNG image of specified dimensions using ImageMagick
*/
function generateImage(width: number, height: number, filename: string): string {
const filepath = join(TEMP_DIR, filename);
async function generateImage(width: number, height: number, filename: string): Promise<string> {
const filepath = path.join(TEMP_DIR, filename);
execSync(`magick -size ${width}x${height} xc:red "${filepath}"`, { stdio: "ignore" });
const buffer = require("fs").readFileSync(filepath);
const buffer = await fs.promises.readFile(filepath);
return buffer.toString("base64");
}
/**
* Generate a valid PNG image of approximately the specified size in bytes
*/
function generateImageWithSize(targetBytes: number, filename: string): string {
const filepath = join(TEMP_DIR, filename);
async function generateImageWithSize(targetBytes: number, filename: string): Promise<string> {
const filepath = path.join(TEMP_DIR, filename);
// Use uncompressed PNG to get predictable sizes
// Each pixel is 3 bytes (RGB), plus PNG overhead (~100 bytes)
// For a square image: side = sqrt(targetBytes / 3)
@@ -101,16 +101,16 @@ function generateImageWithSize(targetBytes: number, filename: string): string {
execSync(`magick -size ${side}x${side} xc: +noise Random -depth 8 PNG24:"${filepath}"`, { stdio: "ignore" });
// Check actual size and adjust if needed
const stats = require("fs").statSync(filepath);
if (stats.size < targetBytes * 0.8) {
const { size } = await Bun.file(filepath).stat();
if (size < targetBytes * 0.8) {
// If too small, increase dimensions
const newSide = Math.ceil(side * Math.sqrt(targetBytes / stats.size));
const newSide = Math.ceil(side * Math.sqrt(targetBytes / size));
execSync(`magick -size ${newSide}x${newSide} xc: +noise Random -depth 8 PNG24:"${filepath}"`, {
stdio: "ignore",
});
}
const buffer = require("fs").readFileSync(filepath);
const buffer = await fs.promises.readFile(filepath);
return buffer.toString("base64");
}
@@ -260,17 +260,17 @@ async function findLimit(
describe("Image Limits E2E Tests", () => {
let smallImage: string; // 100x100 for count tests
beforeAll(() => {
beforeAll(async () => {
// Create temp directory
mkdirSync(TEMP_DIR, { recursive: true });
fs.mkdirSync(TEMP_DIR, { recursive: true });
// Generate small test image for count tests
smallImage = generateImage(100, 100, "small.png");
smallImage = await generateImage(100, 100, "small.png");
});
afterAll(() => {
// Clean up temp directory
rmSync(TEMP_DIR, { recursive: true, force: true });
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
});
// -------------------------------------------------------------------------
@@ -314,7 +314,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -343,7 +343,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -404,7 +404,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -432,7 +432,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -491,7 +491,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -519,7 +519,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -572,7 +572,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -600,7 +600,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -658,7 +658,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -686,7 +686,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -742,7 +742,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -770,7 +770,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -821,7 +821,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -849,7 +849,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -905,7 +905,7 @@ describe("Image Limits E2E Tests", () => {
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const imageBase64 = await generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
@@ -933,7 +933,7 @@ describe("Image Limits E2E Tests", () => {
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const imageBase64 = await generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
@@ -966,12 +966,12 @@ describe("Image Limits E2E Tests", () => {
// Generate images at specific sizes for each provider's limit
const imageCache: Map<number, string> = new Map();
function getImageAtSize(targetMB: number): string {
async function getImageAtSize(targetMB: number): Promise<string> {
if (imageCache.has(targetMB)) {
return imageCache.get(targetMB)!;
}
console.log(` Generating ~${targetMB}MB noise image...`);
const imageBase64 = generateImageWithSize(targetMB * 1024 * 1024, `stress-${targetMB}mb.png`);
const imageBase64 = await generateImageWithSize(targetMB * 1024 * 1024, `stress-${targetMB}mb.png`);
const actualSize = Buffer.from(imageBase64, "base64").length;
console.log(` Actual size: ${(actualSize / 1024 / 1024).toFixed(2)}MB`);
imageCache.set(targetMB, imageBase64);
@@ -984,7 +984,7 @@ describe("Image Limits E2E Tests", () => {
"Anthropic: max ~3MB images before rejection",
async () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const image3mb = getImageAtSize(3);
const image3mb = await getImageAtSize(3);
// 32MB total limit / ~4MB actual = ~8 images
const counts = [1, 2, 4, 6, 8, 10, 12];
@@ -1016,7 +1016,7 @@ describe("Image Limits E2E Tests", () => {
"OpenAI: max ~15MB images before rejection",
async () => {
const model = getModel("openai", "gpt-4o-mini");
const image15mb = getImageAtSize(15);
const image15mb = await getImageAtSize(15);
// Test progressively
const counts = [1, 2, 5, 10, 20];
@@ -1048,7 +1048,7 @@ describe("Image Limits E2E Tests", () => {
"Gemini: max ~20MB images before rejection",
async () => {
const model = getModel("google", "gemini-2.5-flash");
const image20mb = getImageAtSize(20);
const image20mb = await getImageAtSize(20);
// Test progressively
const counts = [1, 2, 5, 10, 20, 50];
@@ -1080,7 +1080,7 @@ describe("Image Limits E2E Tests", () => {
"Mistral: max ~10MB images before rejection",
async () => {
const model = getModel("mistral", "pixtral-12b");
const image10mb = getImageAtSize(10);
const image10mb = await getImageAtSize(10);
// Known limit is 8 images
const counts = [1, 2, 4, 6, 8, 9];
@@ -1112,7 +1112,7 @@ describe("Image Limits E2E Tests", () => {
"xAI: max ~20MB images before rejection",
async () => {
const model = getModel("xai", "grok-2-vision");
const image20mb = getImageAtSize(20);
const image20mb = await getImageAtSize(20);
// Test progressively
const counts = [1, 2, 5, 10, 20];
@@ -1146,7 +1146,7 @@ describe("Image Limits E2E Tests", () => {
const model = getModel("groq", "meta-llama/llama-4-scout-17b-16e-instruct");
// Generate 5760x5760 image (33177600 pixels = Groq's limit)
console.log(" Generating 5760x5760 test image for Groq...");
const image5760 = generateImage(5760, 5760, "stress-5760.png");
const image5760 = await generateImage(5760, 5760, "stress-5760.png");
// Known limit is 5 images
const counts = [1, 2, 3, 4, 5, 6];
@@ -1179,7 +1179,7 @@ describe("Image Limits E2E Tests", () => {
"zAI: max ~15MB images before rejection",
async () => {
const model = getModel("zai", "glm-4.5v");
const image15mb = getImageAtSize(15);
const image15mb = await getImageAtSize(15);
// Context-limited, test progressively
const counts = [1, 2, 5, 10, 20];
@@ -1211,7 +1211,7 @@ describe("Image Limits E2E Tests", () => {
"OpenRouter: max ~5MB images before rejection",
async () => {
const model = getModel("openrouter", "z-ai/glm-4.5v");
const image5mb = getImageAtSize(5);
const image5mb = await getImageAtSize(5);
// Context-limited, test progressively
const counts = [1, 2, 5, 10, 20];
+6 -6
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { Api, Context, Model, Tool, ToolResultMessage } from "@oh-my-pi/pi-ai";
import { complete, getModel } from "@oh-my-pi/pi-ai";
import type { OptionsForApi } from "@oh-my-pi/pi-ai/types";
@@ -32,8 +32,8 @@ async function handleToolWithImageResult<TApi extends Api>(model: Model<TApi>, o
}
// Read the test image
const imagePath = join(import.meta.dir, "data", "red-circle.png");
const imageBuffer = readFileSync(imagePath);
const imagePath = path.join(import.meta.dir, "data", "red-circle.png");
const imageBuffer = await fs.readFile(imagePath);
const base64Image = imageBuffer.toString("base64");
// Define a tool that returns only an image (no text)
@@ -120,8 +120,8 @@ async function handleToolWithTextAndImageResult<TApi extends Api>(model: Model<T
}
// Read the test image
const imagePath = join(import.meta.dir, "data", "red-circle.png");
const imageBuffer = readFileSync(imagePath);
const imagePath = path.join(import.meta.dir, "data", "red-circle.png");
const imageBuffer = await fs.readFile(imagePath);
const base64Image = imageBuffer.toString("base64");
// Define a tool that returns both text and an image
+13 -16
View File
@@ -7,7 +7,8 @@
* E2E tests are disabled by default. Set E2E=1 environment variable to enable.
*/
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import * as fs from "node:fs/promises";
import { isEnoent } from "@oh-my-pi/pi-utils";
/**
* E2E tests require explicit opt-in via E2E=1 environment variable.
@@ -44,25 +45,21 @@ type AuthCredential = ApiKeyCredential | OAuthCredentialEntry;
type AuthStorage = Record<string, AuthCredential>;
function loadAuthStorage(): AuthStorage {
if (!existsSync(AUTH_PATH)) {
return {};
}
async function loadAuthStorage(): Promise<AuthStorage> {
try {
const content = readFileSync(AUTH_PATH, "utf-8");
const content = await Bun.file(AUTH_PATH).text();
return JSON.parse(content);
} catch {
return {};
} catch (err) {
if (isEnoent(err)) return {};
throw err;
}
}
function saveAuthStorage(storage: AuthStorage): void {
async function saveAuthStorage(storage: AuthStorage): Promise<void> {
const configDir = dirname(AUTH_PATH);
if (!existsSync(configDir)) {
mkdirSync(configDir, { recursive: true, mode: 0o700 });
}
writeFileSync(AUTH_PATH, JSON.stringify(storage, null, 2), "utf-8");
chmodSync(AUTH_PATH, 0o600);
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
await Bun.write(AUTH_PATH, JSON.stringify(storage, null, 2));
await fs.chmod(AUTH_PATH, 0o600);
}
/**
@@ -76,7 +73,7 @@ function saveAuthStorage(storage: AuthStorage): void {
export async function resolveApiKey(provider: string): Promise<string | undefined> {
if (!E2E_ENABLED) return undefined;
const storage = loadAuthStorage();
const storage = await loadAuthStorage();
const entry = storage[provider];
if (!entry) return undefined;
@@ -100,7 +97,7 @@ export async function resolveApiKey(provider: string): Promise<string | undefine
// Save refreshed credentials back to auth.json
storage[provider] = { type: "oauth", ...result.newCredentials };
saveAuthStorage(storage);
await saveAuthStorage(storage);
return result.apiKey;
}
+7 -7
View File
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import { streamOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import type { Context, Model } from "@oh-my-pi/pi-ai/types";
import { createTempDirSync } from "@oh-my-pi/pi-utils";
import { TempDir } from "@oh-my-pi/pi-utils";
const originalFetch = global.fetch;
const originalAgentDir = process.env.OMP_CODING_AGENT_DIR;
@@ -18,8 +18,8 @@ afterEach(() => {
describe("openai-codex streaming", () => {
it("streams SSE responses into AssistantMessageEventStream", async () => {
const tempDir = createTempDirSync("@pi-codex-stream-");
process.env.OMP_CODING_AGENT_DIR = tempDir.path;
const tempDir = TempDir.createSync("@pi-codex-stream-");
process.env.OMP_CODING_AGENT_DIR = tempDir.path();
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
@@ -129,8 +129,8 @@ describe("openai-codex streaming", () => {
});
it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => {
const tempDir = createTempDirSync("@pi-codex-stream-");
process.env.OMP_CODING_AGENT_DIR = tempDir.path;
const tempDir = TempDir.createSync("@pi-codex-stream-");
process.env.OMP_CODING_AGENT_DIR = tempDir.path();
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
@@ -230,8 +230,8 @@ describe("openai-codex streaming", () => {
});
it("does not set conversation_id/session_id headers when sessionId is not provided", async () => {
const tempDir = createTempDirSync("@pi-codex-stream-");
process.env.OMP_CODING_AGENT_DIR = tempDir.path;
const tempDir = TempDir.createSync("@pi-codex-stream-");
process.env.OMP_CODING_AGENT_DIR = tempDir.path();
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
+2 -2
View File
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { type ChildProcess, execSync, spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import * as fs from "node:fs/promises";
import { getModel } from "@oh-my-pi/pi-ai/models";
import { complete, stream } from "@oh-my-pi/pi-ai/stream";
import type { Api, Context, ImageContent, Model, OptionsForApi, Tool, ToolResultMessage } from "@oh-my-pi/pi-ai/types";
@@ -219,7 +219,7 @@ async function handleImage<TApi extends Api>(model: Model<TApi>, options?: Optio
// Read the test image
const imagePath = join(import.meta.dir, "data", "red-circle.png");
const imageBuffer = readFileSync(imagePath);
const imageBuffer = await fs.readFile(imagePath);
const base64Image = imageBuffer.toString("base64");
const imageContent: ImageContent = {
+8
View File
@@ -46,6 +46,9 @@
- Added automatic idle kernel session cleanup timer (5-minute timeout, 30-second interval)
- Added types/assets/index.d.ts for global TypeScript module declarations supporting `.md`, `.py`, and `.wasm?raw` imports
- Added bunfig.toml loader configuration for importing markdown, Python, and WASM files as text modules
- Added color manipulation utilities (hexToHsv, hsvToHex, shiftHue) to pi-utils for accessible theme adjustments
- Added color-blind mode setting for improved accessibility
- Added filesystem error type guards (isEnoent, isEacces, isPerm, isEnotempty, isFsError, hasFsCode) to pi-utils for safe error handling
### Changed
- Changed changelog diff truncation limit to be configurable via settings
@@ -102,6 +105,11 @@
- Updated TypeScript and Bun configuration for monorepo-wide build consistency and reduced boilerplate
- Removed WASM base64 encoding build script; imports now use Bun loader with `wasm?raw` query parameter
- Unified TypeScript checking pipeline with tsgo-based configuration instead of per-package tsconfig.publish.json boilerplate
- Migrated file system operations from synchronous to asynchronous APIs in skills loading, agent discovery, and file processing for improved performance
- Refactored scanDirectoryForSkills to use async/await with concurrent directory scanning via Promise.all
- Migrated file system operations from synchronous to asynchronous APIs across plugins, settings, extensions, and config modules
- Updated import organization in model-registry.ts and other modules for consistency
- Changed FilesystemError handling to use new pi-utils type guards for safer error checking
### Fixed
- Fixed database busy errors during concurrent access by adding retry logic with exponential backoff when opening storage
@@ -15,9 +15,9 @@ export default function (pi: HookAPI) {
pi.on("session_start", async (_event, ctx) => {
const triggerFile = "/tmp/agent-trigger.txt";
fs.watch(triggerFile, () => {
fs.watch(triggerFile, async () => {
try {
const content = fs.readFileSync(triggerFile, "utf-8").trim();
const content = (await Bun.file(triggerFile).text()).trim();
if (content) {
pi.sendMessage(
{
@@ -27,7 +27,7 @@ export default function (pi: HookAPI) {
},
true, // triggerTurn - get LLM to respond
);
fs.writeFileSync(triggerFile, ""); // Clear after reading
await Bun.write(triggerFile, ""); // Clear after reading
}
} catch {
// File might not exist yet
+6 -6
View File
@@ -80,29 +80,29 @@
"@oh-my-pi/pi-ai": "workspace:*",
"@oh-my-pi/pi-tui": "workspace:*",
"@oh-my-pi/pi-utils": "workspace:*",
"@openai/agents": "^0.3.7",
"@openai/agents": "^0.4.3",
"@sinclair/typebox": "^0.34.46",
"ajv": "^8.17.1",
"chalk": "^5.5.0",
"cli-highlight": "^2.1.11",
"diff": "^8.0.2",
"file-type": "^21.1.1",
"glob": "^11.0.3",
"glob": "^13.0.0",
"handlebars": "^4.7.8",
"highlight.js": "^11.11.1",
"marked": "^15.0.12",
"marked": "^17.0.1",
"nanoid": "^5.1.6",
"ndjson": "^2.0.0",
"node-html-parser": "^6.1.13",
"node-html-parser": "^7.0.2",
"smol-toml": "^1.6.0",
"strip-ansi": "^7.1.2",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/diff": "^7.0.2",
"@types/diff": "^8.0.0",
"@types/ms": "^2.1.0",
"@types/ndjson": "^2.0.4",
"@types/node": "^24.3.0",
"@types/node": "^25.0.10",
"ms": "^2.1.3"
},
"keywords": [
+18 -19
View File
@@ -1,16 +1,15 @@
import type { Dirent } from "node:fs";
import { readdir } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
const contentCache = new Map<string, string | null>();
const dirCache = new Map<string, Dirent[]>();
const dirCache = new Map<string, fs.Dirent[]>();
function resolvePath(path: string): string {
return resolve(path);
function resolvePath(filePath: string): string {
return path.resolve(filePath);
}
export async function readFile(path: string): Promise<string | null> {
const abs = resolvePath(path);
export async function readFile(filePath: string): Promise<string | null> {
const abs = resolvePath(filePath);
if (contentCache.has(abs)) {
return contentCache.get(abs) ?? null;
}
@@ -25,14 +24,14 @@ export async function readFile(path: string): Promise<string | null> {
}
}
export async function readDirEntries(path: string): Promise<Dirent[]> {
const abs = resolvePath(path);
export async function readDirEntries(dirPath: string): Promise<fs.Dirent[]> {
const abs = resolvePath(dirPath);
if (dirCache.has(abs)) {
return dirCache.get(abs) ?? [];
}
try {
const entries = await readdir(abs, { withFileTypes: true });
const entries = await fs.promises.readdir(abs, { withFileTypes: true });
dirCache.set(abs, entries);
return entries;
} catch {
@@ -41,8 +40,8 @@ export async function readDirEntries(path: string): Promise<Dirent[]> {
}
}
export async function readDir(path: string): Promise<string[]> {
const entries = await readDirEntries(path);
export async function readDir(dirPath: string): Promise<string[]> {
const entries = await readDirEntries(dirPath);
return entries.map((entry) => entry.name);
}
@@ -58,10 +57,10 @@ export async function walkUp(
const entries = await readDirEntries(current);
const entry = entries.find((e) => e.name === name);
if (entry) {
if (file && entry.isFile()) return join(current, name);
if (dir && entry.isDirectory()) return join(current, name);
if (file && entry.isFile()) return path.join(current, name);
if (dir && entry.isDirectory()) return path.join(current, name);
}
const parent = dirname(current);
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
@@ -79,11 +78,11 @@ export function clearCache(): void {
dirCache.clear();
}
export function invalidate(path: string): void {
const abs = resolvePath(path);
export function invalidate(filePath: string): void {
const abs = resolvePath(filePath);
contentCache.delete(abs);
dirCache.delete(abs);
const parent = dirname(abs);
const parent = path.dirname(abs);
if (parent !== abs) {
dirCache.delete(parent);
}
@@ -8,7 +8,7 @@
*/
import { homedir } from "node:os";
import { resolve } from "node:path";
import * as path from "node:path";
import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
import type {
Capability,
@@ -399,10 +399,10 @@ export function reset(): void {
/**
* Invalidate cache for a specific path.
* @param path - Absolute or relative path to invalidate
* @param filePath - Absolute or relative path to invalidate
*/
export function invalidate(path: string, cwd?: string): void {
const resolved = cwd ? resolve(cwd, path) : path;
export function invalidate(filePath: string, cwd?: string): void {
const resolved = cwd ? path.resolve(cwd, filePath) : filePath;
invalidateFs(resolved);
}
+16 -13
View File
@@ -2,9 +2,10 @@
* Process @file CLI arguments into text content and image attachments
*/
import { existsSync, readFileSync, statSync } from "node:fs";
import { resolve } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { ImageContent } from "@oh-my-pi/pi-ai";
import { isEnoent } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { resolveReadPath } from "../tools/path-utils";
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
@@ -28,16 +29,20 @@ export async function processFileArguments(fileArgs: string[], options?: Process
for (const fileArg of fileArgs) {
// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
const absolutePath = resolve(resolveReadPath(fileArg, process.cwd()));
const absolutePath = path.resolve(resolveReadPath(fileArg, process.cwd()));
// Check if file exists and is not empty
if (!existsSync(absolutePath)) {
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
process.exit(1);
// Read file, handling not-found gracefully
let buffer: Buffer;
try {
buffer = await fs.readFile(absolutePath);
} catch (err) {
if (isEnoent(err)) {
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
process.exit(1);
}
throw err;
}
const stats = statSync(absolutePath);
if (stats.size === 0) {
// Skip empty files
if (buffer.length === 0) {
continue;
}
@@ -45,9 +50,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
if (mimeType) {
// Handle image file
const buffer = readFileSync(absolutePath);
const base64Content = buffer.toString("base64");
let attachment: ImageContent;
let dimensionNote: string | undefined;
@@ -87,7 +90,7 @@ export async function processFileArguments(fileArgs: string[], options?: Process
} else {
// Handle text file
try {
const content = readFileSync(absolutePath, "utf-8");
const content = buffer.toString("utf-8");
text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,106 @@
/**
* Jupyter CLI command handlers.
*
* Handles `omp jupyter` subcommand for managing the shared Python gateway.
*/
import chalk from "chalk";
import { APP_NAME } from "../config";
import { getGatewayStatus, shutdownSharedGateway } from "../ipy/gateway-coordinator";
export type JupyterAction = "kill" | "status";
export interface JupyterCommandArgs {
action: JupyterAction;
}
export function parseJupyterArgs(args: string[]): JupyterCommandArgs | undefined {
if (args.length === 0 || args[0] !== "jupyter") {
return undefined;
}
const action = args[1] as JupyterAction | undefined;
if (!action || !["kill", "status"].includes(action)) {
return { action: "status" };
}
return { action };
}
export async function runJupyterCommand(cmd: JupyterCommandArgs): Promise<void> {
switch (cmd.action) {
case "kill":
await runKill();
break;
case "status":
await runStatus();
break;
}
}
async function runKill(): Promise<void> {
const status = await getGatewayStatus();
if (!status.active) {
console.log(chalk.dim("No Jupyter gateway is running"));
return;
}
console.log(`Killing Jupyter gateway (PID ${status.pid})...`);
await shutdownSharedGateway();
console.log(chalk.green("Jupyter gateway stopped"));
}
async function runStatus(): Promise<void> {
const status = await getGatewayStatus();
if (!status.active) {
console.log(chalk.dim("No Jupyter gateway is running"));
return;
}
console.log(chalk.bold("Jupyter Gateway Status\n"));
console.log(` ${chalk.green("●")} Running`);
console.log(` PID: ${status.pid}`);
console.log(` URL: ${status.url}`);
if (status.uptime !== null) {
console.log(` Uptime: ${formatUptime(status.uptime)}`);
}
if (status.pythonPath) {
console.log(` Python: ${status.pythonPath}`);
}
if (status.venvPath) {
console.log(` Venv: ${status.venvPath}`);
}
}
function formatUptime(ms: number): string {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
}
export function printJupyterHelp(): void {
console.log(`${chalk.bold(`${APP_NAME} jupyter`)} - Manage the shared Jupyter gateway
${chalk.bold("Usage:")}
${APP_NAME} jupyter <command>
${chalk.bold("Commands:")}
status Show gateway status (default)
kill Stop the running gateway
${chalk.bold("Examples:")}
${APP_NAME} jupyter # Show status
${APP_NAME} jupyter status # Show status
${APP_NAME} jupyter kill # Stop the gateway
`);
}
+7 -7
View File
@@ -337,7 +337,7 @@ async function handleFeatures(
// Handle modifications
if (flags.enable || flags.disable || flags.set) {
let currentFeatures = new Set(manager.getEnabledFeatures(pluginName) ?? []);
let currentFeatures = new Set((await manager.getEnabledFeatures(pluginName)) ?? []);
if (flags.set) {
// --set replaces all features
@@ -371,7 +371,7 @@ async function handleFeatures(
}
// Display current state
const updatedFeatures = manager.getEnabledFeatures(pluginName);
const updatedFeatures = await manager.getEnabledFeatures(pluginName);
if (flags.json) {
console.log(
@@ -442,7 +442,7 @@ async function handleConfig(
switch (subcommand) {
case "list": {
const settings = manager.getPluginSettings(pluginName);
const settings = await manager.getPluginSettings(pluginName);
const schema = plugin.manifest.settings || {};
if (flags.json) {
@@ -477,7 +477,7 @@ async function handleConfig(
process.exit(1);
}
const settings = manager.getPluginSettings(pluginName);
const settings = await manager.getPluginSettings(pluginName);
const schema = plugin.manifest.settings?.[key];
const value = settings[key] ?? schema?.default;
@@ -512,7 +512,7 @@ async function handleConfig(
}
}
manager.setPluginSetting(pluginName, key, value);
await manager.setPluginSetting(pluginName, key, value);
console.log(chalk.green(`${theme.status.success} Set ${key}`));
break;
}
@@ -523,7 +523,7 @@ async function handleConfig(
process.exit(1);
}
manager.deletePluginSetting(pluginName, key);
await manager.deletePluginSetting(pluginName, key);
console.log(chalk.green(`${theme.status.success} Deleted ${key}`));
break;
}
@@ -540,7 +540,7 @@ async function handleConfigValidate(manager: PluginManager, flags: { json?: bool
const results: Array<{ plugin: string; key: string; error: string }> = [];
for (const plugin of plugins) {
const settings = manager.getPluginSettings(plugin.name);
const settings = await manager.getPluginSettings(plugin.name);
const schema = plugin.manifest.settings || {};
for (const [key, s] of Object.entries(schema)) {
+21 -17
View File
@@ -6,10 +6,11 @@
*/
import { execSync, spawnSync } from "node:child_process";
import { createWriteStream, existsSync, renameSync, unlinkSync } from "node:fs";
import { dirname } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { isEnoent } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { APP_NAME, VERSION } from "../config";
import { theme } from "../modes/theme/theme";
@@ -161,7 +162,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
}
const execPath = process.execPath;
const _execDir = dirname(execPath);
const _execDir = path.dirname(execPath);
const tempPath = `${execPath}.new`;
const backupPath = `${execPath}.bak`;
@@ -173,7 +174,7 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
throw new Error(`Download failed: ${response.statusText}`);
}
const fileStream = createWriteStream(tempPath, { mode: 0o755 });
const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
const nodeStream = Readable.fromWeb(response.body as import("stream/web").ReadableStream);
await pipeline(nodeStream, fileStream);
@@ -181,27 +182,30 @@ async function updateViaBinary(release: ReleaseInfo): Promise<void> {
console.log(chalk.dim("Installing update..."));
try {
// Backup current binary
if (existsSync(backupPath)) {
unlinkSync(backupPath);
try {
await fs.promises.unlink(backupPath);
} catch (err) {
if (!isEnoent(err)) throw err;
}
renameSync(execPath, backupPath);
await fs.promises.rename(execPath, backupPath);
// Move new binary into place
renameSync(tempPath, execPath);
await fs.promises.rename(tempPath, execPath);
// Clean up backup
unlinkSync(backupPath);
await fs.promises.unlink(backupPath);
console.log(chalk.green(`\n${theme.status.success} Updated to ${release.version}`));
console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
} catch (err) {
// Restore from backup if possible
if (existsSync(backupPath) && !existsSync(execPath)) {
renameSync(backupPath, execPath);
const [backupExists, execExists, tempExists] = await Promise.all([
Bun.file(backupPath).exists(),
Bun.file(execPath).exists(),
Bun.file(tempPath).exists(),
]);
if (backupExists && !execExists) {
await fs.promises.rename(backupPath, execPath);
}
if (existsSync(tempPath)) {
unlinkSync(tempPath);
if (tempExists) {
await fs.promises.unlink(tempPath);
}
throw err;
}
@@ -1,4 +1,4 @@
import { basename } from "node:path";
import * as path from "node:path";
import type { CommitType, ConventionalAnalysis, NumstatEntry } from "../../commit/types";
import type { CommitProposal } from "./state";
@@ -40,8 +40,8 @@ function inferTypeFromFiles(numstat: NumstatEntry[]): CommitType {
return "refactor";
}
function getExtension(path: string): string {
const name = basename(path);
function getExtension(filePath: string): string {
const name = path.basename(filePath);
const dotIndex = name.lastIndexOf(".");
return dotIndex >= 0 ? name.slice(dotIndex).toLowerCase() : "";
}
@@ -49,7 +49,7 @@ function getExtension(path: string): string {
export function generateFallbackAnalysis(numstat: NumstatEntry[]): ConventionalAnalysis {
const type = inferTypeFromFiles(numstat);
const details = numstat.slice(0, 3).map((e) => ({
text: `Updated ${basename(e.path)}`,
text: `Updated ${path.basename(e.path)}`,
userVisible: false,
}));
@@ -76,7 +76,7 @@ export function generateFallbackSummary(type: CommitType, numstat: NumstatEntry[
revert: "reverted changes in",
};
const verb = verbMap[type] ?? "updated";
const file = basename(numstat[0]?.path ?? "files");
const file = path.basename(numstat[0]?.path ?? "files");
if (numstat.length === 1) {
return `${verb} ${file}`;
@@ -1,4 +1,4 @@
import { relative } from "node:path";
import * as path from "node:path";
import { createInterface } from "node:readline/promises";
import { applyChangelogProposals } from "../../commit/changelog";
import { detectChangelogBoundaries } from "../../commit/changelog/detect";
@@ -165,10 +165,10 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
writeStdout(` ├─ ${message}`);
},
});
updatedChangelogFiles = updated.map((path) => relative(cwd, path));
updatedChangelogFiles = updated.map((filePath) => path.relative(cwd, filePath));
if (updated.length > 0) {
for (const path of updated) {
writeStdout(` └─ ${path}`);
for (const filePath of updated) {
writeStdout(` └─ ${filePath}`);
}
} else {
writeStdout(" └─ (no changes)");
@@ -1,4 +1,4 @@
import { dirname, resolve } from "node:path";
import * as path from "node:path";
import type { ChangelogBoundary } from "../../commit/types";
const CHANGELOG_NAME = "CHANGELOG.md";
@@ -21,15 +21,15 @@ export async function detectChangelogBoundaries(cwd: string, stagedFiles: string
}
async function findNearestChangelog(cwd: string, filePath: string): Promise<string | null> {
let current = resolve(cwd, dirname(filePath));
const root = resolve(cwd);
let current = path.resolve(cwd, path.dirname(filePath));
const root = path.resolve(cwd);
while (true) {
const candidate = resolve(current, CHANGELOG_NAME);
const candidate = path.resolve(current, CHANGELOG_NAME);
if (await Bun.file(candidate).exists()) {
return candidate;
}
if (current === root) return null;
const parent = dirname(current);
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
@@ -1,4 +1,4 @@
import { relative, resolve } from "node:path";
import * as path from "node:path";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import type { ControlledGit } from "../../commit/git";
@@ -67,7 +67,7 @@ export async function runChangelogFlow({
continue;
}
const existingEntries = formatExistingEntries(unreleased.entries);
const isPackageChangelog = resolve(boundary.changelogPath) !== resolve(cwd, "CHANGELOG.md");
const isPackageChangelog = path.resolve(boundary.changelogPath) !== path.resolve(cwd, "CHANGELOG.md");
const generated = await generateChangelogEntries({
model,
apiKey,
@@ -82,7 +82,7 @@ export async function runChangelogFlow({
const updatedContent = applyChangelogEntries(changelogContent, unreleased, generated.entries);
if (!dryRun) {
await Bun.write(boundary.changelogPath, updatedContent);
await git.stageFiles([relative(cwd, boundary.changelogPath)]);
await git.stageFiles([path.relative(cwd, boundary.changelogPath)]);
}
updated.push(boundary.changelogPath);
}
@@ -127,7 +127,7 @@ export async function applyChangelogProposals({
const updatedContent = applyChangelogEntries(changelogContent, unreleased, normalized, normalizedDeletions);
if (!dryRun) {
await Bun.write(proposal.path, updatedContent);
await git.stageFiles([relative(cwd, proposal.path)]);
await git.stageFiles([path.relative(cwd, proposal.path)]);
}
updated.push(proposal.path);
}
@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto";
import { rm } from "node:fs/promises";
import * as fs from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import type { FileDiff, FileHunks, NumstatEntry } from "../../commit/types";
import { parseDiffHunks, parseFileDiffs, parseFileHunks, parseNumstat } from "./diff";
@@ -134,13 +134,13 @@ export class ControlledGit {
const patch = joinPatch(patchParts);
if (!patch.trim()) return;
const tempPath = join(tmpdir(), `omp-hunks-${randomUUID()}.patch`);
const tempPath = path.join(tmpdir(), `omp-hunks-${randomUUID()}.patch`);
try {
await Bun.write(tempPath, patch);
const result = await runGitCommand(this.cwd, ["apply", "--cached", "--binary", tempPath]);
this.ensureSuccess(result, "git apply --cached");
} finally {
await rm(tempPath, { force: true });
await fs.rm(tempPath, { force: true });
}
}
@@ -1,4 +1,4 @@
import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
import { completeSimple } from "@oh-my-pi/pi-ai";
import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
import fileObserverUserPrompt from "../../commit/prompts/file-observer-user.md" with { type: "text" };
@@ -52,24 +52,13 @@ export async function runMapPhase({ model, apiKey, files, config }: MapPhaseInpu
diff: truncated,
context_header: contextHeader,
});
const request = {
systemPrompt,
messages: [{ role: "user", content: prompt, timestamp: Date.now() }] as Message[],
};
const response = await withRetry(
async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await completeSimple(
model,
{
systemPrompt: systemPrompt,
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
},
{ apiKey, maxTokens: 400, signal: controller.signal },
);
} finally {
clearTimeout(timeout);
}
},
() => completeSimple(model, request, { apiKey, maxTokens: 400, signal: AbortSignal.timeout(timeoutMs) }),
maxRetries,
retryBackoffMs,
);
+2 -2
View File
@@ -1,4 +1,4 @@
import { relative } from "node:path";
import * as path from "node:path";
import type { Api, Model } from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import { renderPromptTemplate } from "../config/prompt-templates";
@@ -86,7 +86,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
const recentCommits = await git.getRecentCommits(RECENT_COMMITS_COUNT);
const contextFiles = await loadProjectContextFiles({ cwd });
const formattedContextFiles = contextFiles.map((file) => ({
path: relative(cwd, file.path),
path: path.relative(cwd, file.path),
content: file.content,
}));
+57 -59
View File
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, statSync } from "node:fs";
import * as fs from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
// Embed package.json at build time for config
import packageJson from "../package.json" with { type: "json" };
@@ -35,11 +35,11 @@ export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
*/
export function getPackageDir(): string {
let dir = import.meta.dir;
while (dir !== dirname(dir)) {
if (existsSync(join(dir, "package.json"))) {
while (dir !== path.dirname(dir)) {
if (fs.existsSync(path.join(dir, "package.json"))) {
return dir;
}
dir = dirname(dir);
dir = path.dirname(dir);
}
// Fallback to cwd (docs/examples won't be found, but that's fine)
return process.cwd();
@@ -47,7 +47,7 @@ export function getPackageDir(): string {
/** Get path to CHANGELOG.md (optional, may not exist in binary) */
export function getChangelogPath(): string {
return resolve(join(getPackageDir(), "CHANGELOG.md"));
return path.resolve(path.join(getPackageDir(), "CHANGELOG.md"));
}
// =============================================================================
@@ -56,27 +56,27 @@ export function getChangelogPath(): string {
/** Get the agent config directory (e.g., ~/.omp/agent/) */
export function getAgentDir(): string {
return process.env[ENV_AGENT_DIR] || join(homedir(), CONFIG_DIR_NAME, "agent");
return process.env[ENV_AGENT_DIR] || path.join(homedir(), CONFIG_DIR_NAME, "agent");
}
/** Get path to user's custom themes directory */
export function getCustomThemesDir(): string {
return join(getAgentDir(), "themes");
return path.join(getAgentDir(), "themes");
}
/** Get path to models.json */
export function getModelsPath(): string {
return join(getAgentDir(), "models.json");
return path.join(getAgentDir(), "models.json");
}
/** Get path to models.yml (preferred over models.json) */
export function getModelsYamlPath(): string {
return join(getAgentDir(), "models.yml");
return path.join(getAgentDir(), "models.yml");
}
/** Get path to auth.json */
export function getAuthPath(): string {
return join(getAgentDir(), "auth.json");
return path.join(getAgentDir(), "auth.json");
}
/**
@@ -85,37 +85,37 @@ export function getAuthPath(): string {
* @returns Absolute path to the agent.db file
*/
export function getAgentDbPath(agentDir: string = getAgentDir()): string {
return join(agentDir, "agent.db");
return path.join(agentDir, "agent.db");
}
/** Get path to tools directory */
export function getToolsDir(): string {
return join(getAgentDir(), "tools");
return path.join(getAgentDir(), "tools");
}
/** Get path to managed binaries directory (fd, rg) */
export function getBinDir(): string {
return join(getAgentDir(), "bin");
return path.join(getAgentDir(), "bin");
}
/** Get path to slash commands directory */
export function getCommandsDir(): string {
return join(getAgentDir(), "commands");
return path.join(getAgentDir(), "commands");
}
/** Get path to prompts directory */
export function getPromptsDir(): string {
return join(getAgentDir(), "prompts");
return path.join(getAgentDir(), "prompts");
}
/** Get path to sessions directory */
export function getSessionsDir(): string {
return join(getAgentDir(), "sessions");
return path.join(getAgentDir(), "sessions");
}
/** Get path to debug log file */
export function getDebugLogPath(): string {
return join(getAgentDir(), `${APP_NAME}-debug.log`);
return path.join(getAgentDir(), `${APP_NAME}-debug.log`);
}
// =============================================================================
@@ -128,7 +128,7 @@ export function getDebugLogPath(): string {
* Project-level: .omp, .pi, .claude, .codex, .gemini
*/
const USER_CONFIG_BASES = priorityList.map(({ dir, globalAgentDir }) => ({
base: () => join(homedir(), globalAgentDir ?? dir),
base: () => path.join(homedir(), globalAgentDir ?? dir),
name: dir,
}));
@@ -177,9 +177,9 @@ export function getConfigDirs(subpath: string, options: GetConfigDirsOptions = {
// User-level directories (highest priority)
if (user) {
for (const { base, name } of USER_CONFIG_BASES) {
const path = join(base(), subpath);
if (!existingOnly || existsSync(path)) {
results.push({ path, source: name, level: "user" });
const resolvedPath = path.resolve(base(), subpath);
if (!existingOnly || fs.existsSync(resolvedPath)) {
results.push({ path: resolvedPath, source: name, level: "user" });
}
}
}
@@ -187,9 +187,9 @@ export function getConfigDirs(subpath: string, options: GetConfigDirsOptions = {
// Project-level directories
if (project) {
for (const { base, name } of PROJECT_CONFIG_BASES) {
const path = resolve(cwd, base, subpath);
if (!existingOnly || existsSync(path)) {
results.push({ path, source: name, level: "project" });
const resolvedPath = path.resolve(cwd, base, subpath);
if (!existingOnly || fs.existsSync(resolvedPath)) {
results.push({ path: resolvedPath, source: name, level: "project" });
}
}
}
@@ -226,25 +226,24 @@ export interface ConfigFileResult<T> {
* console.log(result.content);
* }
*/
export function readConfigFile<T = unknown>(
export async function readConfigFile<T = unknown>(
subpath: string,
options: GetConfigDirsOptions = {},
): ConfigFileResult<T> | undefined {
): Promise<ConfigFileResult<T> | undefined> {
const dirs = getConfigDirs("", { ...options, existingOnly: false });
for (const { path: base, source, level } of dirs) {
const filePath = join(base, subpath);
const filePath = path.join(base, subpath);
try {
if (existsSync(filePath)) {
const content = readFileSync(filePath, "utf-8");
return {
path: filePath,
source,
level,
content: JSON.parse(content) as T,
};
}
const content = await Bun.file(filePath).text();
return {
path: filePath,
source,
level,
content: JSON.parse(content) as T,
};
} catch (error) {
if (isEnoent(error)) continue;
logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
}
}
@@ -256,26 +255,25 @@ export function readConfigFile<T = unknown>(
* Get all existing config files for a subpath (for merging scenarios).
* Returns in priority order (highest first).
*/
export function readAllConfigFiles<T = unknown>(
export async function readAllConfigFiles<T = unknown>(
subpath: string,
options: GetConfigDirsOptions = {},
): ConfigFileResult<T>[] {
): Promise<ConfigFileResult<T>[]> {
const dirs = getConfigDirs("", { ...options, existingOnly: false });
const results: ConfigFileResult<T>[] = [];
for (const { path: base, source, level } of dirs) {
const filePath = join(base, subpath);
const filePath = path.join(base, subpath);
try {
if (existsSync(filePath)) {
const content = readFileSync(filePath, "utf-8");
results.push({
path: filePath,
source,
level,
content: JSON.parse(content) as T,
});
}
const content = await Bun.file(filePath).text();
results.push({
path: filePath,
source,
level,
content: JSON.parse(content) as T,
});
} catch (error) {
if (isEnoent(error)) continue;
logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
}
}
@@ -291,8 +289,8 @@ export function findConfigFile(subpath: string, options: GetConfigDirsOptions =
const dirs = getConfigDirs("", { ...options, existingOnly: false });
for (const { path: base } of dirs) {
const filePath = join(base, subpath);
if (existsSync(filePath)) {
const filePath = path.join(base, subpath);
if (fs.existsSync(filePath)) {
return filePath;
}
}
@@ -310,8 +308,8 @@ export function findConfigFileWithMeta(
const dirs = getConfigDirs("", { ...options, existingOnly: false });
for (const { path: base, source, level } of dirs) {
const filePath = join(base, subpath);
if (existsSync(filePath)) {
const filePath = path.join(base, subpath);
if (fs.existsSync(filePath)) {
return { path: filePath, source, level };
}
}
@@ -325,7 +323,7 @@ export function findConfigFileWithMeta(
async function isDirectory(p: string): Promise<boolean> {
try {
return existsSync(p) && statSync(p).isDirectory();
return (await fs.promises.stat(p)).isDirectory();
} catch {
return false;
}
@@ -348,14 +346,14 @@ export async function findNearestProjectConfigDir(
while (true) {
// Check all config bases at this level, in priority order
for (const { base, name } of PROJECT_CONFIG_BASES) {
const candidate = join(currentDir, base, subpath);
const candidate = path.join(currentDir, base, subpath);
if (await isDirectory(candidate)) {
return { path: candidate, source: name, level: "project" };
}
}
// Move up one directory
const parentDir = dirname(currentDir);
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) break; // Reached root
currentDir = parentDir;
}
@@ -381,14 +379,14 @@ export async function findAllNearestProjectConfigDirs(
for (const { base, name } of PROJECT_CONFIG_BASES) {
if (foundBases.has(name)) continue;
const candidate = join(currentDir, base, subpath);
const candidate = path.join(currentDir, base, subpath);
if (await isDirectory(candidate)) {
results.push({ path: candidate, source: name, level: "project" });
foundBases.add(name);
}
}
const parentDir = dirname(currentDir);
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) break;
currentDir = parentDir;
}
+16 -6
View File
@@ -1,5 +1,5 @@
import { existsSync } from "node:fs";
import { mkdir, readFile, rm } from "node:fs/promises";
import * as fs from "node:fs/promises";
import { isEnoent } from "@oh-my-pi/pi-utils";
export interface FileLockOptions {
staleMs?: number;
@@ -29,7 +29,7 @@ async function writeLockInfo(lockPath: string): Promise<void> {
async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
try {
const content = await readFile(`${lockPath}/info`, "utf-8");
const content = await fs.readFile(`${lockPath}/info`, "utf-8");
return JSON.parse(content) as LockInfo;
} catch {
return null;
@@ -58,7 +58,7 @@ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean>
async function tryAcquireLock(lockPath: string): Promise<boolean> {
try {
await mkdir(lockPath);
await fs.mkdir(lockPath);
await writeLockInfo(lockPath);
return true;
} catch (error) {
@@ -71,12 +71,22 @@ async function tryAcquireLock(lockPath: string): Promise<boolean> {
async function releaseLock(lockPath: string): Promise<void> {
try {
await rm(lockPath, { recursive: true });
await fs.rm(lockPath, { recursive: true });
} catch {
// Ignore errors on release
}
}
async function lockExists(lockPath: string): Promise<boolean> {
try {
await fs.stat(lockPath);
return true;
} catch (err) {
if (isEnoent(err)) return false;
throw err;
}
}
async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
const opts = { ...DEFAULT_OPTIONS, ...options };
const lockPath = getLockPath(filePath);
@@ -86,7 +96,7 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
return () => releaseLock(lockPath);
}
if (existsSync(lockPath) && (await isLockStale(lockPath, opts.staleMs))) {
if ((await lockExists(lockPath)) && (await isLockStale(lockPath, opts.staleMs))) {
await releaseLock(lockPath);
continue;
}
@@ -1,5 +1,4 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import * as path from "node:path";
import {
DEFAULT_EDITOR_KEYBINDINGS,
type EditorAction,
@@ -162,7 +161,7 @@ export class KeybindingsManager {
* Create from config file and set up editor keybindings.
*/
static async create(agentDir: string = getAgentDir()): Promise<KeybindingsManager> {
const configPath = join(agentDir, "keybindings.json");
const configPath = path.join(agentDir, "keybindings.json");
const config = await KeybindingsManager.loadFromFile(configPath);
const manager = new KeybindingsManager(config);
@@ -186,9 +185,8 @@ export class KeybindingsManager {
}
private static async loadFromFile(path: string): Promise<KeybindingsConfig> {
if (!existsSync(path)) return {};
try {
return JSON.parse(readFileSync(path, "utf-8"));
return await Bun.file(path).json();
} catch (error) {
logger.warn("Failed to parse keybindings config", { path, error: String(error) });
return {};
@@ -2,8 +2,8 @@
* Model registry - manages built-in and custom models, provides API key resolution.
*/
import { existsSync, readFileSync } from "node:fs";
import { extname } from "node:path";
import * as path from "node:path";
import {
type Api,
getGitHubCopilotBaseUrl,
@@ -12,7 +12,8 @@ import {
type Model,
normalizeDomain,
} from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import AjvModule from "ajv";
import { YAML } from "bun";
@@ -104,10 +105,12 @@ interface CustomModelsResult {
/** Providers with only baseUrl/headers override (no custom models) */
overrides: Map<string, ProviderOverride>;
error: string | undefined;
/** Whether the file was found (true) or didn't exist (false) */
found: boolean;
}
function emptyCustomModelsResult(error?: string): CustomModelsResult {
return { models: [], replacedProviders: new Set(), overrides: new Map(), error };
return { models: [], replacedProviders: new Set(), overrides: new Map(), error, found: false };
}
/**
@@ -146,8 +149,6 @@ export class ModelRegistry {
}
return undefined;
});
// Load models synchronously in constructor
this.loadModels();
}
/**
@@ -190,10 +191,10 @@ export class ModelRegistry {
/**
* Reload models from disk (built-in + custom from models.json).
*/
refresh(): void {
async refresh(): Promise<void> {
this.customProviderApiKeys.clear();
this.loadError = undefined;
this.loadModels();
await this.loadModels();
}
/**
@@ -203,7 +204,7 @@ export class ModelRegistry {
return this.loadError;
}
private loadModels(): void {
private async loadModels(): Promise<void> {
// Load custom models from models.json first (to know which providers to skip/override)
let customModels: Model<Api>[] = [];
let replacedProviders: Set<string> = new Set();
@@ -215,19 +216,20 @@ export class ModelRegistry {
}
for (const modelsPath of pathsToCheck) {
if (existsSync(modelsPath)) {
logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
const result = this.loadCustomModels(modelsPath);
if (result.error) {
this.loadError = result.error;
// Keep built-in models even if custom models failed to load
} else {
customModels = result.models;
replacedProviders = result.replacedProviders;
overrides = result.overrides;
}
break; // Use first existing file
const result = await this.loadCustomModels(modelsPath);
if (!result.found) {
continue; // File doesn't exist, try next path
}
logger.debug("ModelRegistry.loadModels loading", { path: modelsPath });
if (result.error) {
this.loadError = result.error;
// Keep built-in models even if custom models failed to load
} else {
customModels = result.models;
replacedProviders = result.replacedProviders;
overrides = result.overrides;
}
break; // Use first existing file
}
const builtInModels = this.loadBuiltInModels(replacedProviders, overrides);
@@ -264,14 +266,24 @@ export class ModelRegistry {
});
}
private loadCustomModels(modelsPath: string): CustomModelsResult {
if (!existsSync(modelsPath)) {
return emptyCustomModelsResult();
private async loadCustomModels(modelsPath: string): Promise<CustomModelsResult> {
let content: string;
try {
content = await Bun.file(modelsPath).text();
} catch (error) {
if (isEnoent(error)) {
return emptyCustomModelsResult();
}
return {
...emptyCustomModelsResult(
`Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
),
found: true,
};
}
try {
const content = readFileSync(modelsPath, "utf-8");
const ext = extname(modelsPath).toLowerCase();
const ext = path.extname(modelsPath).toLowerCase();
let config: ModelsConfig;
if (ext === ".yaml" || ext === ".yml") {
@@ -315,14 +327,20 @@ export class ModelRegistry {
}
}
return { models: this.parseModels(config), replacedProviders, overrides, error: undefined };
return { models: this.parseModels(config), replacedProviders, overrides, error: undefined, found: true };
} catch (error) {
if (error instanceof SyntaxError) {
return emptyCustomModelsResult(`Failed to parse models config: ${error.message}\n\nFile: ${modelsPath}`);
return {
...emptyCustomModelsResult(`Failed to parse models config: ${error.message}\n\nFile: ${modelsPath}`),
found: true,
};
}
return emptyCustomModelsResult(
`Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
);
return {
...emptyCustomModelsResult(
`Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
),
found: true,
};
}
}
@@ -1,4 +1,4 @@
import { join, resolve } from "node:path";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import Handlebars from "handlebars";
import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
@@ -382,7 +382,7 @@ async function loadTemplatesFromDir(
entries.sort((a, b) => a.split("/").length - b.split("/").length);
for (const entry of entries) {
const fullPath = join(dir, entry);
const fullPath = path.join(dir, entry);
const file = Bun.file(fullPath);
try {
@@ -461,11 +461,11 @@ export async function loadPromptTemplates(options: LoadPromptTemplatesOptions =
// 1. Load global templates from agentDir/prompts/
// Note: if agentDir is provided, it should be the agent dir, not the prompts dir
const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
const globalPromptsDir = options.agentDir ? path.join(options.agentDir, "prompts") : resolvedAgentDir;
templates.push(...(await loadTemplatesFromDir(globalPromptsDir, "user")));
// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
const projectPromptsDir = path.resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
templates.push(...(await loadTemplatesFromDir(projectPromptsDir, "project")));
return templates;
@@ -1,7 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import { rename } from "node:fs/promises";
import { join } from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
import { getAgentDbPath, getAgentDir } from "../config";
@@ -203,6 +202,7 @@ export interface Settings {
interruptMode?: "immediate" | "wait";
theme?: string;
symbolPreset?: SymbolPreset; // default: uses theme's preset or "unicode"
colorBlindMode?: boolean; // default: false (use blue instead of green for diff additions)
compaction?: CompactionSettings;
branchSummary?: BranchSummarySettings;
retry?: RetrySettings;
@@ -503,7 +503,7 @@ export class SettingsManager {
* @returns Configured SettingsManager with merged global and user settings
*/
static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
const configPath = join(agentDir, "config.yml");
const configPath = path.join(agentDir, "config.yml");
const storage = await AgentStorage.open(getAgentDbPath(agentDir));
// Migrate from legacy storage if config.yml doesn't exist
@@ -521,7 +521,7 @@ export class SettingsManager {
}
// Load persisted settings from config.yml
const storedSettings = SettingsManager.loadFromYaml(configPath);
const storedSettings = await SettingsManager.loadFromYaml(configPath);
globalSettings = deepMergeSettings(globalSettings, storedSettings);
// Load project settings before construction (constructor is sync)
@@ -559,18 +559,19 @@ export class SettingsManager {
* @param configPath - Path to config.yml, or null for in-memory mode
* @returns Parsed and migrated settings, or empty object if file doesn't exist
*/
private static loadFromYaml(configPath: string | null): Settings {
if (!configPath || !existsSync(configPath)) {
private static async loadFromYaml(configPath: string | null): Promise<Settings> {
if (!configPath) {
return {};
}
try {
const content = readFileSync(configPath, "utf-8");
const content = await Bun.file(configPath).text();
const parsed = YAML.parse(content);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return {};
}
return SettingsManager.migrateSettings(parsed as Record<string, unknown>);
} catch (error) {
if (isEnoent(error)) return {};
logger.warn("SettingsManager failed to load config.yml", { path: configPath, error: String(error) });
return {};
}
@@ -582,31 +583,34 @@ export class SettingsManager {
* Only migrates if config.yml doesn't exist.
*/
private static async migrateToYaml(storage: AgentStorage, agentDir: string, configPath: string): Promise<void> {
// Skip if config.yml already exists
if (existsSync(configPath)) return;
try {
await Bun.file(configPath).text();
return;
} catch (err) {
if (!isEnoent(err)) throw err;
}
let settings: Settings = {};
let migrated = false;
// 1. Try to migrate from settings.json (oldest legacy format)
const settingsJsonPath = join(agentDir, "settings.json");
const settingsJsonPath = path.join(agentDir, "settings.json");
try {
const settingsFile = Bun.file(settingsJsonPath);
if (await settingsFile.exists()) {
const parsed = JSON.parse(await settingsFile.text());
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
settings = deepMergeSettings(settings, SettingsManager.migrateSettings(parsed));
migrated = true;
// Backup settings.json
try {
await rename(settingsJsonPath, `${settingsJsonPath}.bak`);
} catch (error) {
logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
}
const parsed = JSON.parse(await Bun.file(settingsJsonPath).text());
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
settings = deepMergeSettings(settings, SettingsManager.migrateSettings(parsed));
migrated = true;
// Backup settings.json
try {
await fs.rename(settingsJsonPath, `${settingsJsonPath}.bak`);
} catch (error) {
logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
}
}
} catch (error) {
logger.warn("SettingsManager failed to read settings.json", { error: String(error) });
if (!isEnoent(error)) {
logger.warn("SettingsManager failed to read settings.json", { error: String(error) });
}
}
// 2. Migrate from agent.db settings table
@@ -690,7 +694,7 @@ export class SettingsManager {
const configPath = this.configPath;
try {
await withFileLock(configPath, async () => {
const currentSettings = SettingsManager.loadFromYaml(configPath);
const currentSettings = await SettingsManager.loadFromYaml(configPath);
const mergedSettings = deepMergeSettings(currentSettings, this.globalSettings);
this.globalSettings = mergedSettings;
await Bun.write(configPath, YAML.stringify(this.globalSettings, null, 2));
@@ -788,6 +792,15 @@ export class SettingsManager {
await this.save();
}
getColorBlindMode(): boolean {
return this.settings.colorBlindMode ?? false;
}
async setColorBlindMode(enabled: boolean): Promise<void> {
this.globalSettings.colorBlindMode = enabled;
await this.save();
}
getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
return this.settings.defaultThinkingLevel;
}
+8 -6
View File
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import { rmSync, statSync } from "node:fs";
import * as fs from "node:fs/promises";
import type {
AgentEvent,
AgentTool,
@@ -98,17 +98,19 @@ async function executeDelete(options: CursorExecBridgeOptions, pathArg: string,
let result: AgentToolResult<unknown>;
try {
const stat = statSync(absolutePath, { throwIfNoEntry: false });
if (!stat) {
let fileStat: Awaited<ReturnType<typeof fs.stat>> | undefined;
try {
fileStat = await fs.stat(absolutePath);
} catch {
throw new Error(`File not found: ${pathArg}`);
}
if (!stat.isFile()) {
if (!fileStat.isFile()) {
throw new Error(`Path is not a file: ${pathArg}`);
}
rmSync(absolutePath);
await fs.rm(absolutePath);
const sizeText = stat.size ? ` (${stat.size} bytes)` : "";
const sizeText = fileStat.size ? ` (${fileStat.size} bytes)` : "";
const message = `Deleted ${pathArg}${sizeText}`;
result = { content: [{ type: "text", text: message }], details: {} };
} catch (error) {
@@ -6,7 +6,7 @@
* like .codex/ or .gemini/, which are handled by their respective providers).
*/
import { dirname, join, sep } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { readFile } from "../capability/fs";
@@ -29,16 +29,16 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
let depth = 0;
while (depth < MAX_DEPTH) {
const candidate = join(current, "AGENTS.md");
const candidate = path.join(current, "AGENTS.md");
const content = await readFile(candidate);
if (content !== null) {
const parent = dirname(candidate);
const baseName = parent.split(sep).pop() ?? "";
const parent = path.dirname(candidate);
const baseName = parent.split(path.sep).pop() ?? "";
if (!baseName.startsWith(".")) {
const fileDir = dirname(candidate);
const calculatedDepth = calculateDepth(ctx.cwd, fileDir, sep);
const fileDir = path.dirname(candidate);
const calculatedDepth = calculateDepth(ctx.cwd, fileDir, path.sep);
items.push({
path: candidate,
@@ -51,7 +51,7 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
}
// Move to parent directory
const parent = dirname(current);
const parent = path.dirname(current);
if (parent === current) break; // Reached filesystem root
current = parent;
depth++;
+26 -26
View File
@@ -5,7 +5,7 @@
* Priority: 80 (tool-specific, below builtin but above shared standards)
*/
import { join, sep } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -38,14 +38,14 @@ const CONFIG_DIR = ".claude";
* Get user-level .claude path.
*/
function getUserClaude(ctx: LoadContext): string {
return join(ctx.home, CONFIG_DIR);
return path.join(ctx.home, CONFIG_DIR);
}
/**
* Get project-level .claude path (cwd only).
*/
function getProjectClaude(ctx: LoadContext): string {
return join(ctx.cwd, CONFIG_DIR);
return path.join(ctx.cwd, CONFIG_DIR);
}
// =============================================================================
@@ -57,12 +57,12 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userClaudeJson = join(ctx.home, ".claude.json");
const userMcpJson = join(userBase, "mcp.json");
const userClaudeJson = path.join(ctx.home, ".claude.json");
const userMcpJson = path.join(userBase, "mcp.json");
const projectBase = join(ctx.cwd, CONFIG_DIR);
const projectMcpJson = join(projectBase, ".mcp.json");
const projectMcpJsonAlt = join(projectBase, "mcp.json");
const projectBase = path.join(ctx.cwd, CONFIG_DIR);
const projectMcpJson = path.join(projectBase, ".mcp.json");
const projectMcpJsonAlt = path.join(projectBase, "mcp.json");
const userPaths = [
{ path: userClaudeJson, level: "user" as const },
@@ -126,7 +126,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userClaudeMd = join(userBase, "CLAUDE.md");
const userClaudeMd = path.join(userBase, "CLAUDE.md");
const userContent = await readFile(userClaudeMd);
if (userContent !== null) {
@@ -139,10 +139,10 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
}
const projectBase = getProjectClaude(ctx);
const projectClaudeMd = join(projectBase, "CLAUDE.md");
const projectClaudeMd = path.join(projectBase, "CLAUDE.md");
const projectContent = await readFile(projectClaudeMd);
if (projectContent !== null) {
const depth = calculateDepth(ctx.cwd, projectBase, sep);
const depth = calculateDepth(ctx.cwd, projectBase, path.sep);
items.push({
path: projectClaudeMd,
content: projectContent,
@@ -160,8 +160,8 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
// =============================================================================
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
const userSkillsDir = join(getUserClaude(ctx), "skills");
const projectSkillsDir = join(getProjectClaude(ctx), "skills");
const userSkillsDir = path.join(getUserClaude(ctx), "skills");
const projectSkillsDir = path.join(getProjectClaude(ctx), "skills");
const results = await Promise.all([
loadSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
@@ -183,8 +183,8 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userExtensionsDir = join(userBase, "extensions");
const projectExtensionsDir = join(ctx.cwd, CONFIG_DIR, "extensions");
const userExtensionsDir = path.join(userBase, "extensions");
const projectExtensionsDir = path.join(ctx.cwd, CONFIG_DIR, "extensions");
const dirsToDiscover: { dir: string; level: "user" | "project" }[] = [
{ dir: userExtensionsDir, level: "user" },
@@ -221,7 +221,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userCommandsDir = join(userBase, "commands");
const userCommandsDir = path.join(userBase, "commands");
const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
extensions: ["md"],
@@ -240,7 +240,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
items.push(...userResult.items);
if (userResult.warnings) warnings.push(...userResult.warnings);
const projectCommandsDir = join(ctx.cwd, CONFIG_DIR, "commands");
const projectCommandsDir = path.join(ctx.cwd, CONFIG_DIR, "commands");
const projectResult = await loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
extensions: ["md"],
@@ -271,18 +271,18 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userHooksDir = join(userBase, "hooks");
const userHooksDir = path.join(userBase, "hooks");
const projectBase = getProjectClaude(ctx);
const projectHooksDir = join(projectBase, "hooks");
const projectHooksDir = path.join(projectBase, "hooks");
const hookTypes = ["pre", "post"] as const;
const loadTasks: { dir: string; hookType: "pre" | "post"; level: "user" | "project" }[] = [];
for (const hookType of hookTypes) {
loadTasks.push({ dir: join(userHooksDir, hookType), hookType, level: "user" });
loadTasks.push({ dir: path.join(userHooksDir, hookType), hookType, level: "user" });
}
for (const hookType of hookTypes) {
loadTasks.push({ dir: join(projectHooksDir, hookType), hookType, level: "project" });
loadTasks.push({ dir: path.join(projectHooksDir, hookType), hookType, level: "project" });
}
const results = await Promise.all(
@@ -320,7 +320,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userToolsDir = join(userBase, "tools");
const userToolsDir = path.join(userBase, "tools");
const userResult = await loadFilesFromDir<CustomTool>(ctx, userToolsDir, PROVIDER_ID, "user", {
transform: (name, _content, path, source) => {
@@ -339,7 +339,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
if (userResult.warnings) warnings.push(...userResult.warnings);
const projectBase = getProjectClaude(ctx);
const projectToolsDir = join(projectBase, "tools");
const projectToolsDir = path.join(projectBase, "tools");
const projectResult = await loadFilesFromDir<CustomTool>(ctx, projectToolsDir, PROVIDER_ID, "project", {
transform: (name, _content, path, source) => {
@@ -369,7 +369,7 @@ async function loadSystemPrompts(ctx: LoadContext): Promise<LoadResult<SystemPro
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userSystemMd = join(userBase, "SYSTEM.md");
const userSystemMd = path.join(userBase, "SYSTEM.md");
const content = await readFile(userSystemMd);
if (content !== null) {
@@ -393,7 +393,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
const warnings: string[] = [];
const userBase = getUserClaude(ctx);
const userSettingsJson = join(userBase, "settings.json");
const userSettingsJson = path.join(userBase, "settings.json");
const userContent = await readFile(userSettingsJson);
if (userContent) {
@@ -411,7 +411,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
}
const projectBase = getProjectClaude(ctx);
const projectSettingsJson = join(projectBase, "settings.json");
const projectSettingsJson = path.join(projectBase, "settings.json");
const projectContent = await readFile(projectSettingsJson);
if (projectContent) {
const data = parseJSON<Record<string, unknown>>(projectContent);
+4 -4
View File
@@ -5,7 +5,7 @@
* Project-only (no user-level config).
*/
import { dirname, resolve } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { readDirEntries, readFile } from "../capability/fs";
import type { Rule } from "../capability/rule";
@@ -19,18 +19,18 @@ const DISPLAY_NAME = "Cline";
const PRIORITY = 40;
async function findClinerules(startDir: string): Promise<{ path: string; isDir: boolean } | null> {
let current = resolve(startDir);
let current = path.resolve(startDir);
while (true) {
const entries = await readDirEntries(current);
const entry = entries.find((e) => e.name === ".clinerules");
if (entry) {
return {
path: resolve(current, ".clinerules"),
path: path.resolve(current, ".clinerules"),
isDir: entry.isDirectory(),
};
}
const parent = dirname(current);
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
}
+19 -19
View File
@@ -7,7 +7,7 @@
* User directory: ~/.codex
*/
import { join } from "node:path";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { parse as parseToml } from "smol-toml";
import { registerProvider } from "../capability";
@@ -45,7 +45,7 @@ const DISPLAY_NAME = "OpenAI Codex";
const PRIORITY = 70;
function getProjectCodexDir(ctx: LoadContext): string {
return join(ctx.cwd, ".codex");
return path.join(ctx.cwd, ".codex");
}
// =============================================================================
@@ -57,7 +57,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
const warnings: string[] = [];
// User level only: ~/.codex/AGENTS.md
const agentsMd = join(ctx.home, SOURCE_PATHS.codex.userBase, "AGENTS.md");
const agentsMd = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "AGENTS.md");
const agentsContent = await readFile(agentsMd);
if (agentsContent) {
items.push({
@@ -78,9 +78,9 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
const warnings: string[] = [];
const userConfigPath = join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
const userConfigPath = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
const codexDir = getProjectCodexDir(ctx);
const projectConfigPath = join(codexDir, "config.toml");
const projectConfigPath = path.join(codexDir, "config.toml");
const [userConfig, projectConfig] = await Promise.all([
loadTomlConfig(ctx, userConfigPath),
@@ -210,9 +210,9 @@ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string
// =============================================================================
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
const userSkillsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "skills");
const userSkillsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "skills");
const codexDir = getProjectCodexDir(ctx);
const projectSkillsDir = join(codexDir, "skills");
const projectSkillsDir = path.join(codexDir, "skills");
const results = await Promise.all([
loadSkillsFromDir(ctx, {
@@ -240,9 +240,9 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
const warnings: string[] = [];
const userExtensionsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "extensions");
const userExtensionsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "extensions");
const codexDir = getProjectCodexDir(ctx);
const projectExtensionsDir = join(codexDir, "extensions");
const projectExtensionsDir = path.join(codexDir, "extensions");
const [userPaths, projectPaths] = await Promise.all([
discoverExtensionModulePaths(ctx, userExtensionsDir),
@@ -272,9 +272,9 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
// =============================================================================
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
const userCommandsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "commands");
const userCommandsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "commands");
const codexDir = getProjectCodexDir(ctx);
const projectCommandsDir = join(codexDir, "commands");
const projectCommandsDir = path.join(codexDir, "commands");
const transformCommand =
(level: "user" | "project") =>
@@ -312,9 +312,9 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
// =============================================================================
async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
const userPromptsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "prompts");
const userPromptsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "prompts");
const codexDir = getProjectCodexDir(ctx);
const projectPromptsDir = join(codexDir, "prompts");
const projectPromptsDir = path.join(codexDir, "prompts");
const transformPrompt = (
name: string,
@@ -355,9 +355,9 @@ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
// =============================================================================
async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
const userHooksDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "hooks");
const userHooksDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "hooks");
const codexDir = getProjectCodexDir(ctx);
const projectHooksDir = join(codexDir, "hooks");
const projectHooksDir = path.join(codexDir, "hooks");
const transformHook =
(level: "user" | "project") =>
@@ -398,9 +398,9 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
// =============================================================================
async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
const userToolsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "tools");
const userToolsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "tools");
const codexDir = getProjectCodexDir(ctx);
const projectToolsDir = join(codexDir, "tools");
const projectToolsDir = path.join(codexDir, "tools");
const transformTool =
(level: "user" | "project") =>
@@ -438,9 +438,9 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
const warnings: string[] = [];
const userConfigPath = join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
const userConfigPath = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
const codexDir = getProjectCodexDir(ctx);
const projectConfigPath = join(codexDir, "config.toml");
const projectConfigPath = path.join(codexDir, "config.toml");
const [userConfig, projectConfig] = await Promise.all([
loadTomlConfig(ctx, userConfigPath),
@@ -16,7 +16,7 @@
* - settings: From settings.json
*/
import { join, sep } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
@@ -146,7 +146,7 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
const content = await readFile(projectGeminiMd);
if (content) {
const projectBase = getProjectPath(ctx, "gemini", "");
const depth = projectBase ? calculateDepth(ctx.cwd, projectBase, sep) : 0;
const depth = projectBase ? calculateDepth(ctx.cwd, projectBase, path.sep) : 0;
items.push({
path: projectGeminiMd,
@@ -194,8 +194,8 @@ async function loadExtensionsFromDir(extensionsDir: string, level: "user" | "pro
const results = await Promise.all(
dirEntries.map(async (entry) => {
const extPath = join(extensionsDir, entry.name);
const manifestPath = join(extPath, "gemini-extension.json");
const extPath = path.join(extensionsDir, entry.name);
const manifestPath = path.join(extPath, "gemini-extension.json");
const content = await readFile(manifestPath);
return { entry, extPath, manifestPath, content };
}),
@@ -12,7 +12,7 @@
* - instructions: *.instructions.md in .github/instructions/ with applyTo frontmatter
*/
import { basename, dirname, sep } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { readFile } from "../capability/fs";
@@ -37,8 +37,8 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
if (copilotInstructionsPath) {
const content = await readFile(copilotInstructionsPath);
if (content) {
const fileDir = dirname(copilotInstructionsPath);
const depth = calculateDepth(ctx.cwd, fileDir, sep);
const fileDir = path.dirname(copilotInstructionsPath);
const depth = calculateDepth(ctx.cwd, fileDir, path.sep);
items.push({
path: copilotInstructionsPath,
@@ -74,23 +74,23 @@ async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instructio
return { items, warnings };
}
function transformInstruction(name: string, content: string, path: string, source: SourceMeta): Instruction | null {
function transformInstruction(name: string, content: string, filePath: string, source: SourceMeta): Instruction | null {
// Only process .instructions.md files
if (!name.endsWith(".instructions.md")) {
return null;
}
const { frontmatter, body } = parseFrontmatter(content, { source: path });
const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
// Extract applyTo glob pattern from frontmatter
const applyTo = typeof frontmatter.applyTo === "string" ? frontmatter.applyTo : undefined;
// Derive name from filename (strip .instructions.md suffix)
const instructionName = basename(name, ".instructions.md");
const instructionName = path.basename(name, ".instructions.md");
return {
name: instructionName,
path,
path: filePath,
content: body,
applyTo,
_source: source,
+19 -17
View File
@@ -3,7 +3,7 @@
*/
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { readDirEntries, readFile } from "../capability/fs";
import type { Skill, SkillFrontmatter } from "../capability/skill";
@@ -26,10 +26,10 @@ export function normalizeUnicodeSpaces(str: string): string {
export function expandPath(p: string): string {
const normalized = normalizeUnicodeSpaces(p);
if (normalized.startsWith("~/")) {
return join(homedir(), normalized.slice(2));
return path.join(homedir(), normalized.slice(2));
}
if (normalized.startsWith("~")) {
return join(homedir(), normalized.slice(1));
return path.join(homedir(), normalized.slice(1));
}
return normalized;
}
@@ -94,7 +94,7 @@ export type SourceId = keyof typeof SOURCE_PATHS;
export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string): string | null {
const paths = SOURCE_PATHS[source];
if (!paths.userAgent) return null;
return join(ctx.home, paths.userAgent, subpath);
return path.join(ctx.home, paths.userAgent, subpath);
}
/**
@@ -104,17 +104,17 @@ export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: stri
const paths = SOURCE_PATHS[source];
if (!paths.projectDir) return null;
return join(ctx.cwd, paths.projectDir, subpath);
return path.join(ctx.cwd, paths.projectDir, subpath);
}
/**
* Create source metadata for an item.
*/
export function createSourceMeta(provider: string, path: string, level: "user" | "project"): SourceMeta {
export function createSourceMeta(provider: string, filePath: string, level: "user" | "project"): SourceMeta {
return {
provider,
providerName: "", // Filled in by registry
path: resolve(path),
path: path.resolve(filePath),
level,
};
}
@@ -229,7 +229,7 @@ export async function loadSkillsFromDir(
const results = await Promise.all(
skillDirs.map(async (entry) => {
const skillFile = join(dir, entry.name, "SKILL.md");
const skillFile = path.join(dir, entry.name, "SKILL.md");
const content = await readFile(skillFile);
if (!content) {
return { item: null as Skill | null, warning: null as string | null };
@@ -326,12 +326,14 @@ export async function loadFilesFromDir<T>(
});
const [subResults, fileResults] = await Promise.all([
Promise.all(directories.map((entry) => loadFilesFromDir(_ctx, join(dir, entry.name), provider, level, options))),
Promise.all(
directories.map((entry) => loadFilesFromDir(_ctx, path.join(dir, entry.name), provider, level, options)),
),
Promise.all(
files.map(async (entry) => {
const path = join(dir, entry.name);
const content = await readFile(path);
return { entry, path, content };
const filePath = path.join(dir, entry.name);
const content = await readFile(filePath);
return { entry, path: filePath, content };
}),
),
]);
@@ -427,7 +429,7 @@ export async function discoverExtensionModulePaths(ctx: LoadContext, dir: string
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
const entryPath = join(dir, entry.name);
const entryPath = path.join(dir, entry.name);
// 1. Direct files: *.ts or *.js
if (entry.isFile() && isExtensionModuleFile(entry.name)) {
@@ -442,11 +444,11 @@ export async function discoverExtensionModulePaths(ctx: LoadContext, dir: string
// Check for package.json with "omp"/"pi" field first
if (subFileNames.has("package.json")) {
const packageJsonPath = join(entryPath, "package.json");
const packageJsonPath = path.join(entryPath, "package.json");
const manifest = await readExtensionModuleManifest(ctx, packageJsonPath);
if (manifest?.extensions && Array.isArray(manifest.extensions)) {
for (const extPath of manifest.extensions) {
const resolvedExtPath = resolve(entryPath, extPath);
const resolvedExtPath = path.resolve(entryPath, extPath);
const content = await readFile(resolvedExtPath);
if (content !== null) {
discovered.push(resolvedExtPath);
@@ -458,9 +460,9 @@ export async function discoverExtensionModulePaths(ctx: LoadContext, dir: string
// Check for index.ts or index.js
if (subFileNames.has("index.ts")) {
discovered.push(join(entryPath, "index.ts"));
discovered.push(path.join(entryPath, "index.ts"));
} else if (subFileNames.has("index.js")) {
discovered.push(join(entryPath, "index.js"));
discovered.push(path.join(entryPath, "index.js"));
}
}
}
@@ -7,7 +7,7 @@
* Priority: 5 (low, as this is a fallback after tool-specific providers)
*/
import { join } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { readFile } from "../capability/fs";
import { type MCPServer, mcpCapability } from "../capability/mcp";
@@ -102,7 +102,7 @@ async function loadMCPJsonFile(
async function load(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
const filenames = ["mcp.json", ".mcp.json"];
const results = await Promise.all(
filenames.map((filename) => loadMCPJsonFile(ctx, join(ctx.cwd, filename), "project")),
filenames.map((filename) => loadMCPJsonFile(ctx, path.join(ctx.cwd, filename), "project")),
);
const allItems = results.flatMap((r) => r.items);
+2 -2
View File
@@ -5,7 +5,7 @@
* Priority: 5 (low, project-level only)
*/
import { join } from "node:path";
import * as path from "node:path";
import { registerProvider } from "../capability";
import { readFile } from "../capability/fs";
import { type SSHHost, sshCapability } from "../capability/ssh";
@@ -134,7 +134,7 @@ async function loadSshJsonFile(_ctx: LoadContext, path: string): Promise<LoadRes
async function load(ctx: LoadContext): Promise<LoadResult<SSHHost>> {
const filenames = ["ssh.json", ".ssh.json"];
const results = await Promise.all(filenames.map((filename) => loadSshJsonFile(ctx, join(ctx.cwd, filename))));
const results = await Promise.all(filenames.map((filename) => loadSshJsonFile(ctx, path.join(ctx.cwd, filename))));
const allItems = results.flatMap((r) => r.items);
const allWarnings = results.flatMap((r) => r.warnings ?? []);
+10 -10
View File
@@ -4,10 +4,10 @@
* Client for interacting with Exa MCP servers.
*/
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import type { TSchema } from "@sinclair/typebox";
import type { CustomTool, CustomToolResult } from "../extensibility/custom-tools/types";
import { callMCP } from "../mcp/json-rpc";
import type {
@@ -34,15 +34,15 @@ export async function findApiKey(): Promise<string | null> {
for (const dir of [cwd, home]) {
const envPath = `${dir}/.env`;
try {
if (existsSync(envPath)) {
const content = readFileSync(envPath, "utf-8");
const match = content.match(/^EXA_API_KEY=(.+)$/m);
if (match?.[1]) {
return match[1].trim().replace(/^["']|["']$/g, "");
}
const content = await Bun.file(envPath).text();
const match = content.match(/^EXA_API_KEY=(.+)$/m);
if (match?.[1]) {
return match[1].trim().replace(/^["']|["']$/g, "");
}
} catch (err) {
if (!isEnoent(err)) {
logger.debug("Error reading .env file", { path: envPath, error: String(err) });
}
} catch {
// Ignore read errors
}
}
@@ -5,8 +5,8 @@
* that will be used instead of the default GitHub Gist sharing.
*/
import { existsSync } from "node:fs";
import { join } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { getAgentDir } from "../config";
export interface CustomShareResult {
@@ -32,8 +32,8 @@ export function getCustomSharePath(): string | null {
const agentDir = getAgentDir();
for (const candidate of SHARE_SCRIPT_CANDIDATES) {
const scriptPath = join(agentDir, candidate);
if (existsSync(scriptPath)) {
const scriptPath = path.join(agentDir, candidate);
if (fs.existsSync(scriptPath)) {
return scriptPath;
}
}
+20 -16
View File
@@ -1,6 +1,6 @@
import { existsSync, writeFileSync } from "node:fs";
import { basename } from "node:path";
import * as path from "node:path";
import type { AgentState } from "@oh-my-pi/pi-agent-core";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { APP_NAME } from "../../config";
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
import { SessionManager } from "../../session/session-manager";
@@ -74,14 +74,14 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string
}
/** Generate CSS custom properties for theme. */
function generateThemeVars(themeName?: string): string {
const colors = getResolvedThemeColors(themeName);
async function generateThemeVars(themeName?: string): Promise<string> {
const colors = await getResolvedThemeColors(themeName);
const lines: string[] = [];
for (const [key, value] of Object.entries(colors)) {
lines.push(`--${key}: ${value};`);
}
const themeExport = getThemeExportColors(themeName);
const themeExport = await getThemeExportColors(themeName);
const userMessageBg = colors.userMessageBg || "#343541";
const derived = deriveExportColors(userMessageBg);
@@ -101,8 +101,8 @@ interface SessionData {
}
/** Generate HTML from bundled template with runtime substitutions. */
function generateHtml(sessionData: SessionData, themeName?: string): string {
const themeVars = generateThemeVars(themeName);
async function generateHtml(sessionData: SessionData, themeName?: string): Promise<string> {
const themeVars = await generateThemeVars(themeName);
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
return TEMPLATE.replace("<theme-vars/>", `<style>:root { ${themeVars} }</style>`).replace(
@@ -121,7 +121,6 @@ export async function exportSessionToHtml(
const sessionFile = sm.getSessionFile();
if (!sessionFile) throw new Error("Cannot export in-memory session to HTML");
if (!existsSync(sessionFile)) throw new Error("Nothing to export yet - start a conversation first");
const sessionData: SessionData = {
header: sm.getHeader(),
@@ -131,10 +130,10 @@ export async function exportSessionToHtml(
tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })),
};
const html = generateHtml(sessionData, opts.themeName);
const outputPath = opts.outputPath || `${APP_NAME}-session-${basename(sessionFile, ".jsonl")}.html`;
const html = await generateHtml(sessionData, opts.themeName);
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`;
writeFileSync(outputPath, html, "utf8");
await Bun.write(outputPath, html);
return outputPath;
}
@@ -142,18 +141,23 @@ export async function exportSessionToHtml(
export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
if (!existsSync(inputPath)) throw new Error(`File not found: ${inputPath}`);
let sm: SessionManager;
try {
sm = await SessionManager.open(inputPath);
} catch (err) {
if (isEnoent(err)) throw new Error(`File not found: ${inputPath}`);
throw err;
}
const sm = await SessionManager.open(inputPath);
const sessionData: SessionData = {
header: sm.getHeader(),
entries: sm.getEntries(),
leafId: sm.getLeafId(),
};
const html = generateHtml(sessionData, opts.themeName);
const outputPath = opts.outputPath || `${APP_NAME}-session-${basename(inputPath, ".jsonl")}.html`;
const html = await generateHtml(sessionData, opts.themeName);
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
writeFileSync(outputPath, html, "utf8");
await Bun.write(outputPath, html);
return outputPath;
}
@@ -5,10 +5,10 @@
* to avoid import resolution issues with custom commands loaded from user directories.
*/
import { type Dirent, existsSync, readdirSync } from "node:fs";
import * as fs from "node:fs";
import * as path from "node:path";
import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
import { logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import * as typebox from "@sinclair/typebox";
import { getAgentDir, getConfigDirs } from "../../config";
import { execCommand } from "../../exec/exec";
@@ -77,7 +77,9 @@ export interface DiscoverCustomCommandsResult {
* Discover custom command modules (TypeScript slash commands).
* Markdown slash commands are handled by core/slash-commands.ts.
*/
export function discoverCustomCommands(options: DiscoverCustomCommandsOptions = {}): DiscoverCustomCommandsResult {
export async function discoverCustomCommands(
options: DiscoverCustomCommandsOptions = {},
): Promise<DiscoverCustomCommandsResult> {
const cwd = options.cwd ?? process.cwd();
const agentDir = options.agentDir ?? getAgentDir();
const paths: Array<{ path: string; source: CustomCommandSource }> = [];
@@ -93,7 +95,7 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions =
const commandDirs: Array<{ path: string; source: CustomCommandSource }> = [];
if (agentDir) {
const userCommandsDir = path.join(agentDir, "commands");
if (existsSync(userCommandsDir)) {
if (fs.existsSync(userCommandsDir)) {
commandDirs.push({ path: userCommandsDir, source: "user" });
}
}
@@ -107,11 +109,13 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions =
const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
for (const { path: commandsDir, source } of commandDirs) {
let entries: Dirent[];
let entries: fs.Dirent[];
try {
entries = readdirSync(commandsDir, { withFileTypes: true });
entries = await fs.promises.readdir(commandsDir, { withFileTypes: true });
} catch (error) {
logger.warn("Failed to read custom commands directory", { path: commandsDir, error: String(error) });
if (!isEnoent(error)) {
logger.warn("Failed to read custom commands directory", { path: commandsDir, error: String(error) });
}
continue;
}
for (const entry of entries) {
@@ -120,7 +124,7 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions =
for (const filename of indexCandidates) {
const candidate = path.join(commandDir, filename);
if (existsSync(candidate)) {
if (fs.existsSync(candidate)) {
addPath(candidate, source);
break;
}
@@ -162,7 +166,7 @@ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}
const cwd = options.cwd ?? process.cwd();
const agentDir = options.agentDir ?? getAgentDir();
const { paths } = discoverCustomCommands({ cwd, agentDir });
const { paths } = await discoverCustomCommands({ cwd, agentDir });
const commands: LoadedCustomCommand[] = [];
const errors: Array<{ path: string; error: string }> = [];
@@ -223,7 +223,7 @@ export async function discoverAndLoadCustomTools(configuredPaths: string[], cwd:
}
// 2. Plugin tools: ~/.omp/plugins/node_modules/*/
for (const pluginPath of getAllPluginToolPaths(cwd)) {
for (const pluginPath of await getAllPluginToolPaths(cwd)) {
addPath(pluginPath, { provider: "plugin", providerName: "Plugin", level: "user" });
}
@@ -2,13 +2,14 @@
* Extension loader - loads TypeScript extension modules using native Bun import.
*/
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import type * as fs1 from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
import type { KeyId } from "@oh-my-pi/pi-tui";
import { logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import type { TSchema } from "@sinclair/typebox";
import * as TypeBox from "@sinclair/typebox";
import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
@@ -316,10 +317,9 @@ interface ExtensionManifest {
skills?: string[];
}
function readExtensionManifest(packageJsonPath: string): ExtensionManifest | null {
async function readExtensionManifest(packageJsonPath: string): Promise<ExtensionManifest | null> {
try {
const content = readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content) as { omp?: ExtensionManifest; pi?: ExtensionManifest };
const pkg = (await Bun.file(packageJsonPath).json()) as { omp?: ExtensionManifest; pi?: ExtensionManifest };
const manifest = pkg.omp ?? pkg.pi;
if (manifest && typeof manifest === "object") {
return manifest;
@@ -338,31 +338,38 @@ function isExtensionFile(name: string): boolean {
/**
* Resolve extension entry points from a directory.
*/
function resolveExtensionEntries(dir: string): string[] | null {
async function resolveExtensionEntries(dir: string): Promise<string[] | null> {
const packageJsonPath = path.join(dir, "package.json");
if (existsSync(packageJsonPath)) {
const manifest = readExtensionManifest(packageJsonPath);
if (manifest?.extensions?.length) {
const entries: string[] = [];
for (const extPath of manifest.extensions) {
const resolvedExtPath = path.resolve(dir, extPath);
if (existsSync(resolvedExtPath)) {
entries.push(resolvedExtPath);
}
}
if (entries.length > 0) {
return entries;
const manifest = await readExtensionManifest(packageJsonPath);
if (manifest?.extensions?.length) {
const entries: string[] = [];
for (const extPath of manifest.extensions) {
const resolvedExtPath = path.resolve(dir, extPath);
try {
await fs.stat(resolvedExtPath);
entries.push(resolvedExtPath);
} catch (err) {
if (!isEnoent(err)) throw err;
}
}
if (entries.length > 0) {
return entries;
}
}
const indexTs = path.join(dir, "index.ts");
const indexJs = path.join(dir, "index.js");
if (existsSync(indexTs)) {
try {
await fs.stat(indexTs);
return [indexTs];
} catch (err) {
if (!isEnoent(err)) throw err;
}
if (existsSync(indexJs)) {
try {
await fs.stat(indexJs);
return [indexJs];
} catch (err) {
if (!isEnoent(err)) throw err;
}
return null;
@@ -378,36 +385,32 @@ function resolveExtensionEntries(dir: string): string[] | null {
*
* No recursion beyond one level. Complex packages must use package.json manifest.
*/
function discoverExtensionsInDir(dir: string): string[] {
if (!existsSync(dir)) {
async function discoverExtensionsInDir(dir: string): Promise<string[]> {
const discovered: string[] = [];
let entries: fs1.Dirent[];
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch (err) {
if (isEnoent(err)) return [];
logger.warn("Failed to discover extensions in directory", { path: dir, error: String(err) });
return [];
}
const discovered: string[] = [];
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
try {
const entries = readdirSync(dir, { withFileTypes: true });
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
discovered.push(entryPath);
continue;
}
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
// 1. Direct files: *.ts or *.js
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
discovered.push(entryPath);
continue;
}
// 2 & 3. Subdirectories
if (entry.isDirectory() || entry.isSymbolicLink()) {
const entries = resolveExtensionEntries(entryPath);
if (entries) {
discovered.push(...entries);
}
if (entry.isDirectory() || entry.isSymbolicLink()) {
const resolved = await resolveExtensionEntries(entryPath);
if (resolved) {
discovered.push(...resolved);
}
}
} catch (error) {
logger.warn("Failed to discover extensions in directory", { path: dir, error: String(error) });
return [];
}
return discovered;
@@ -454,14 +457,22 @@ export async function discoverAndLoadExtensions(
// 2. Explicitly configured paths
for (const configuredPath of configuredPaths) {
const resolved = resolvePath(configuredPath, cwd);
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
const entries = resolveExtensionEntries(resolved);
let stat: fs1.Stats | null = null;
try {
stat = await fs.stat(resolved);
} catch (err) {
if (!isEnoent(err)) throw err;
}
if (stat?.isDirectory()) {
const entries = await resolveExtensionEntries(resolved);
if (entries) {
addPaths(entries);
continue;
}
const discovered = discoverExtensionsInDir(resolved);
const discovered = await discoverExtensionsInDir(resolved);
if (discovered.length > 0) {
addPaths(discovered);
}
@@ -98,9 +98,9 @@ const noOpUIContext: ExtensionUIContext = {
get theme() {
return theme;
},
getAllThemes: () => [],
getTheme: () => undefined,
setTheme: (_theme: string | Theme) => ({ success: false, error: "UI not available" }),
getAllThemes: () => Promise.resolve([]),
getTheme: () => Promise.resolve(undefined),
setTheme: (_theme: string | Theme) => Promise.resolve({ success: false, error: "UI not available" }),
};
export class ExtensionRunner {
@@ -112,13 +112,13 @@ export interface ExtensionUIContext {
readonly theme: Theme;
/** Get all available themes with names and paths. */
getAllThemes(): { name: string; path: string | undefined }[];
getAllThemes(): Promise<{ name: string; path: string | undefined }[]>;
/** Load a theme by name without switching to it. */
getTheme(name: string): Theme | undefined;
getTheme(name: string): Promise<Theme | undefined>;
/** Set the current theme by name or Theme object. */
setTheme(theme: string | Theme): { success: boolean; error?: string };
setTheme(theme: string | Theme): Promise<{ success: boolean; error?: string }>;
}
// ============================================================================
@@ -1,10 +1,10 @@
import { lstatSync, symlinkSync, unlinkSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "../../config";
import type { InstalledPlugin } from "./types";
const PLUGINS_DIR = join(getAgentDir(), "plugins");
const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
// Valid npm package name pattern (scoped and unscoped)
const VALID_PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9-._^~>=<]+)?$/i;
@@ -26,8 +26,8 @@ function validatePackageName(name: string): void {
* Ensure the plugins directory exists
*/
async function ensurePluginsDir(): Promise<void> {
await mkdir(PLUGINS_DIR, { recursive: true });
await mkdir(join(PLUGINS_DIR, "node_modules"), { recursive: true });
await fs.mkdir(PLUGINS_DIR, { recursive: true });
await fs.mkdir(path.join(PLUGINS_DIR, "node_modules"), { recursive: true });
}
export async function installPlugin(packageName: string): Promise<InstalledPlugin> {
@@ -38,7 +38,7 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
await ensurePluginsDir();
// Initialize package.json if it doesn't exist
const pkgJsonPath = join(PLUGINS_DIR, "package.json");
const pkgJsonPath = path.join(PLUGINS_DIR, "package.json");
const pkgJson = Bun.file(pkgJsonPath);
if (!(await pkgJson.exists())) {
await pkgJson.write(JSON.stringify({ name: "omp-plugins", private: true, dependencies: {} }, null, 2));
@@ -62,7 +62,7 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
const actualName = packageName.replace(/@[^/]+$/, "").replace(/^(@[^/]+\/[^@]+).*$/, "$1");
// Read the installed package's package.json
const pkgPath = join(PLUGINS_DIR, "node_modules", actualName, "package.json");
const pkgPath = path.join(PLUGINS_DIR, "node_modules", actualName, "package.json");
const pkgFile = Bun.file(pkgPath);
if (!(await pkgFile.exists())) {
throw new Error(`Package installed but package.json not found at ${pkgPath}`);
@@ -73,7 +73,7 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
return {
name: pkg.name,
version: pkg.version,
path: join(PLUGINS_DIR, "node_modules", actualName),
path: path.join(PLUGINS_DIR, "node_modules", actualName),
manifest: pkg.omp || pkg.pi || { version: pkg.version },
enabledFeatures: null,
enabled: true,
@@ -100,7 +100,7 @@ export async function uninstallPlugin(name: string): Promise<void> {
}
export async function listPlugins(): Promise<InstalledPlugin[]> {
const pkgJsonPath = Bun.file(join(PLUGINS_DIR, "package.json"));
const pkgJsonPath = Bun.file(path.join(PLUGINS_DIR, "package.json"));
if (!(await pkgJsonPath.exists())) {
return [];
}
@@ -110,14 +110,14 @@ export async function listPlugins(): Promise<InstalledPlugin[]> {
const plugins: InstalledPlugin[] = [];
for (const [name, _version] of Object.entries(deps)) {
const path = join(PLUGINS_DIR, "node_modules", name);
const fpkg = Bun.file(join(path, "package.json"));
const pluginPath = path.join(PLUGINS_DIR, "node_modules", name);
const fpkg = Bun.file(path.join(pluginPath, "package.json"));
if (await fpkg.exists()) {
const pkg = await fpkg.json();
plugins.push({
name,
version: pkg.version,
path,
path: pluginPath,
manifest: pkg.omp || pkg.pi || { version: pkg.version },
enabledFeatures: null,
enabled: true,
@@ -130,17 +130,17 @@ export async function listPlugins(): Promise<InstalledPlugin[]> {
export async function linkPlugin(localPath: string): Promise<void> {
const cwd = process.cwd();
const absolutePath = resolve(cwd, localPath);
const absolutePath = path.resolve(cwd, localPath);
// Validate that resolved path is within cwd to prevent path traversal
const normalizedCwd = resolve(cwd);
const normalizedPath = resolve(absolutePath);
const normalizedCwd = path.resolve(cwd);
const normalizedPath = path.resolve(absolutePath);
if (!normalizedPath.startsWith(`${normalizedCwd}/`) && normalizedPath !== normalizedCwd) {
throw new Error(`Invalid path: ${localPath} resolves outside working directory`);
}
// Validate package.json exists
const pkgFile = Bun.file(join(absolutePath, "package.json"));
const pkgFile = Bun.file(path.join(absolutePath, "package.json"));
if (!(await pkgFile.exists())) {
throw new Error(`package.json not found at ${absolutePath}`);
}
@@ -167,24 +167,24 @@ export async function linkPlugin(localPath: string): Promise<void> {
await ensurePluginsDir();
// Create symlink in plugins/node_modules
const linkPath = join(PLUGINS_DIR, "node_modules", pkg.name);
const linkPath = path.join(PLUGINS_DIR, "node_modules", pkg.name);
// For scoped packages, ensure the scope directory exists
if (pkg.name.startsWith("@")) {
const scopeDir = join(PLUGINS_DIR, "node_modules", pkg.name.split("/")[0]);
await mkdir(scopeDir, { recursive: true });
const scopeDir = path.join(PLUGINS_DIR, "node_modules", pkg.name.split("/")[0]);
await fs.mkdir(scopeDir, { recursive: true });
}
// Remove existing if present
try {
const stat = lstatSync(linkPath);
if (stat.isSymbolicLink() || stat.isDirectory()) {
unlinkSync(linkPath);
const stats = await fs.lstat(linkPath);
if (stats.isSymbolicLink() || stats.isDirectory()) {
await fs.unlink(linkPath);
}
} catch {
// Doesn't exist, that's fine
} catch (err) {
if (!isEnoent(err)) throw err;
}
// Create symlink using fs instead of shell command
symlinkSync(absolutePath, linkPath);
await fs.symlink(absolutePath, linkPath);
}
@@ -5,8 +5,9 @@
* based on manifest entries and enabled features.
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import {
getAllProjectPluginOverridePaths,
getPluginsLockfile,
@@ -22,29 +23,26 @@ import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPlugi
/**
* Load plugin runtime config from lock file.
*/
function loadRuntimeConfig(): PluginRuntimeConfig {
async function loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
const lockPath = getPluginsLockfile();
if (!existsSync(lockPath)) {
return { plugins: {}, settings: {} };
}
try {
return JSON.parse(readFileSync(lockPath, "utf-8"));
} catch {
return { plugins: {}, settings: {} };
return await Bun.file(lockPath).json();
} catch (err) {
if (isEnoent(err)) return { plugins: {}, settings: {} };
throw err;
}
}
/**
* Load project-local plugin overrides (checks .omp and .pi directories).
*/
function loadProjectOverrides(cwd: string): ProjectPluginOverrides {
async function loadProjectOverrides(cwd: string): Promise<ProjectPluginOverrides> {
for (const overridesPath of getAllProjectPluginOverridePaths(cwd)) {
if (existsSync(overridesPath)) {
try {
return JSON.parse(readFileSync(overridesPath, "utf-8"));
} catch {
// Continue to next path
}
try {
return await Bun.file(overridesPath).json();
} catch (err) {
if (isEnoent(err)) continue;
// JSON parse error - continue to next path
}
}
return {};
@@ -58,30 +56,36 @@ function loadProjectOverrides(cwd: string): ProjectPluginOverrides {
* Get list of enabled plugins with their resolved configurations.
* Respects both global runtime config and project overrides.
*/
export function getEnabledPlugins(cwd: string): InstalledPlugin[] {
export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]> {
const pkgJsonPath = getPluginsPackageJson();
if (!existsSync(pkgJsonPath)) {
return [];
let pkg: { dependencies?: Record<string, string> };
try {
pkg = await Bun.file(pkgJsonPath).json();
} catch (err) {
if (isEnoent(err)) return [];
throw err;
}
const nodeModulesPath = getPluginsNodeModules();
if (!existsSync(nodeModulesPath)) {
if (!fs.existsSync(nodeModulesPath)) {
return [];
}
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
const deps = pkg.dependencies || {};
const runtimeConfig = loadRuntimeConfig();
const projectOverrides = loadProjectOverrides(cwd);
const runtimeConfig = await loadRuntimeConfig();
const projectOverrides = await loadProjectOverrides(cwd);
const plugins: InstalledPlugin[] = [];
for (const [name] of Object.entries(deps)) {
const pluginPkgPath = join(nodeModulesPath, name, "package.json");
if (!existsSync(pluginPkgPath)) {
continue;
const pluginPkgPath = path.join(nodeModulesPath, name, "package.json");
let pluginPkg: { version: string; omp?: PluginManifest; pi?: PluginManifest };
try {
pluginPkg = await Bun.file(pluginPkgPath).json();
} catch (err) {
if (isEnoent(err)) continue;
throw err;
}
const pluginPkg = JSON.parse(readFileSync(pluginPkgPath, "utf-8"));
const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;
if (!manifest) {
@@ -109,7 +113,7 @@ export function getEnabledPlugins(cwd: string): InstalledPlugin[] {
plugins.push({
name,
version: pluginPkg.version,
path: join(nodeModulesPath, name),
path: path.join(nodeModulesPath, name),
manifest,
enabledFeatures,
enabled: true,
@@ -133,8 +137,8 @@ export function resolvePluginToolPaths(plugin: InstalledPlugin): string[] {
// Base tools entry (always included if exists)
if (manifest.tools) {
const toolPath = join(plugin.path, manifest.tools);
if (existsSync(toolPath)) {
const toolPath = path.join(plugin.path, manifest.tools);
if (fs.existsSync(toolPath)) {
paths.push(toolPath);
}
}
@@ -148,8 +152,8 @@ export function resolvePluginToolPaths(plugin: InstalledPlugin): string[] {
if (feat.tools) {
for (const toolEntry of feat.tools) {
const toolPath = join(plugin.path, toolEntry);
if (existsSync(toolPath)) {
const toolPath = path.join(plugin.path, toolEntry);
if (fs.existsSync(toolPath)) {
paths.push(toolPath);
}
}
@@ -162,8 +166,8 @@ export function resolvePluginToolPaths(plugin: InstalledPlugin): string[] {
if (feat.tools) {
for (const toolEntry of feat.tools) {
const toolPath = join(plugin.path, toolEntry);
if (existsSync(toolPath)) {
const toolPath = path.join(plugin.path, toolEntry);
if (fs.existsSync(toolPath)) {
paths.push(toolPath);
}
}
@@ -184,8 +188,8 @@ export function resolvePluginHookPaths(plugin: InstalledPlugin): string[] {
// Base hooks entry (always included if exists)
if (manifest.hooks) {
const hookPath = join(plugin.path, manifest.hooks);
if (existsSync(hookPath)) {
const hookPath = path.join(plugin.path, manifest.hooks);
if (fs.existsSync(hookPath)) {
paths.push(hookPath);
}
}
@@ -199,8 +203,8 @@ export function resolvePluginHookPaths(plugin: InstalledPlugin): string[] {
if (feat.hooks) {
for (const hookEntry of feat.hooks) {
const hookPath = join(plugin.path, hookEntry);
if (existsSync(hookPath)) {
const hookPath = path.join(plugin.path, hookEntry);
if (fs.existsSync(hookPath)) {
paths.push(hookPath);
}
}
@@ -213,8 +217,8 @@ export function resolvePluginHookPaths(plugin: InstalledPlugin): string[] {
if (feat.hooks) {
for (const hookEntry of feat.hooks) {
const hookPath = join(plugin.path, hookEntry);
if (existsSync(hookPath)) {
const hookPath = path.join(plugin.path, hookEntry);
if (fs.existsSync(hookPath)) {
paths.push(hookPath);
}
}
@@ -236,8 +240,8 @@ export function resolvePluginCommandPaths(plugin: InstalledPlugin): string[] {
// Base commands (always included if exists)
if (manifest.commands) {
for (const cmdEntry of manifest.commands) {
const cmdPath = join(plugin.path, cmdEntry);
if (existsSync(cmdPath)) {
const cmdPath = path.join(plugin.path, cmdEntry);
if (fs.existsSync(cmdPath)) {
paths.push(cmdPath);
}
}
@@ -252,8 +256,8 @@ export function resolvePluginCommandPaths(plugin: InstalledPlugin): string[] {
if (feat.commands) {
for (const cmdEntry of feat.commands) {
const cmdPath = join(plugin.path, cmdEntry);
if (existsSync(cmdPath)) {
const cmdPath = path.join(plugin.path, cmdEntry);
if (fs.existsSync(cmdPath)) {
paths.push(cmdPath);
}
}
@@ -266,8 +270,8 @@ export function resolvePluginCommandPaths(plugin: InstalledPlugin): string[] {
if (feat.commands) {
for (const cmdEntry of feat.commands) {
const cmdPath = join(plugin.path, cmdEntry);
if (existsSync(cmdPath)) {
const cmdPath = path.join(plugin.path, cmdEntry);
if (fs.existsSync(cmdPath)) {
paths.push(cmdPath);
}
}
@@ -285,8 +289,8 @@ export function resolvePluginCommandPaths(plugin: InstalledPlugin): string[] {
/**
* Get all tool paths from all enabled plugins.
*/
export function getAllPluginToolPaths(cwd: string): string[] {
const plugins = getEnabledPlugins(cwd);
export async function getAllPluginToolPaths(cwd: string): Promise<string[]> {
const plugins = await getEnabledPlugins(cwd);
const paths: string[] = [];
for (const plugin of plugins) {
@@ -299,8 +303,8 @@ export function getAllPluginToolPaths(cwd: string): string[] {
/**
* Get all hook paths from all enabled plugins.
*/
export function getAllPluginHookPaths(cwd: string): string[] {
const plugins = getEnabledPlugins(cwd);
export async function getAllPluginHookPaths(cwd: string): Promise<string[]> {
const plugins = await getEnabledPlugins(cwd);
const paths: string[] = [];
for (const plugin of plugins) {
@@ -313,8 +317,8 @@ export function getAllPluginHookPaths(cwd: string): string[] {
/**
* Get all command paths from all enabled plugins.
*/
export function getAllPluginCommandPaths(cwd: string): string[] {
const plugins = getEnabledPlugins(cwd);
export async function getAllPluginCommandPaths(cwd: string): Promise<string[]> {
const plugins = await getEnabledPlugins(cwd);
const paths: string[] = [];
for (const plugin of plugins) {
@@ -328,9 +332,9 @@ export function getAllPluginCommandPaths(cwd: string): string[] {
* Get plugin settings for use in tool/hook contexts.
* Merges global settings with project overrides.
*/
export function getPluginSettings(pluginName: string, cwd: string): Record<string, unknown> {
const runtimeConfig = loadRuntimeConfig();
const projectOverrides = loadProjectOverrides(cwd);
export async function getPluginSettings(pluginName: string, cwd: string): Promise<Record<string, unknown>> {
const runtimeConfig = await loadRuntimeConfig();
const projectOverrides = await loadProjectOverrides(cwd);
const global = runtimeConfig.settings[pluginName] || {};
const project = projectOverrides.settings?.[pluginName] || {};
@@ -1,5 +1,6 @@
import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { extractPackageName, parsePluginSpec } from "./parser";
import {
getPluginsDir,
@@ -46,44 +47,47 @@ function validatePackageName(name: string): void {
// =============================================================================
export class PluginManager {
private runtimeConfig: PluginRuntimeConfig;
private runtimeConfig: PluginRuntimeConfig | null = null;
private cwd: string;
constructor(cwd: string = process.cwd()) {
this.cwd = cwd;
this.runtimeConfig = this.loadRuntimeConfig();
}
// ==========================================================================
// Runtime Config Management
// ==========================================================================
private loadRuntimeConfig(): PluginRuntimeConfig {
private async loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
const lockPath = getPluginsLockfile();
if (!existsSync(lockPath)) {
return { plugins: {}, settings: {} };
}
try {
return JSON.parse(readFileSync(lockPath, "utf-8"));
} catch {
return { plugins: {}, settings: {} };
return await Bun.file(lockPath).json();
} catch (err) {
if (isEnoent(err)) return { plugins: {}, settings: {} };
throw err;
}
}
private saveRuntimeConfig(): void {
this.ensurePluginsDir();
writeFileSync(getPluginsLockfile(), JSON.stringify(this.runtimeConfig, null, 2));
private async ensureConfigLoaded(): Promise<PluginRuntimeConfig> {
if (!this.runtimeConfig) {
this.runtimeConfig = await this.loadRuntimeConfig();
}
return this.runtimeConfig;
}
private loadProjectOverrides(): ProjectPluginOverrides {
private async saveRuntimeConfig(): Promise<void> {
await this.ensureConfigLoaded();
await this.ensurePluginsDir();
await Bun.write(getPluginsLockfile(), JSON.stringify(this.runtimeConfig, null, 2));
}
private async loadProjectOverrides(): Promise<ProjectPluginOverrides> {
const overridesPath = getProjectPluginOverrides(this.cwd);
if (!existsSync(overridesPath)) {
return {};
}
try {
return JSON.parse(readFileSync(overridesPath, "utf-8"));
} catch {
return {};
return await Bun.file(overridesPath).json();
} catch (err) {
if (isEnoent(err)) return {};
throw err;
}
}
@@ -91,33 +95,33 @@ export class PluginManager {
// Directory Management
// ==========================================================================
private ensurePluginsDir(): void {
const dir = getPluginsDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
const nodeModules = getPluginsNodeModules();
if (!existsSync(nodeModules)) {
mkdirSync(nodeModules, { recursive: true });
}
private async ensurePluginsDir(): Promise<void> {
await fs.promises.mkdir(getPluginsDir(), { recursive: true });
await fs.promises.mkdir(getPluginsNodeModules(), { recursive: true });
}
private ensurePackageJson(): void {
this.ensurePluginsDir();
private async ensurePackageJson(): Promise<void> {
await this.ensurePluginsDir();
const pkgJsonPath = getPluginsPackageJson();
if (!existsSync(pkgJsonPath)) {
writeFileSync(
pkgJsonPath,
JSON.stringify(
{
name: "omp-plugins",
private: true,
dependencies: {},
},
null,
2,
),
);
try {
await Bun.file(pkgJsonPath).json();
} catch (err) {
if (isEnoent(err)) {
await Bun.write(
pkgJsonPath,
JSON.stringify(
{
name: "omp-plugins",
private: true,
dependencies: {},
},
null,
2,
),
);
return;
}
throw err;
}
}
@@ -136,7 +140,7 @@ export class PluginManager {
const spec = parsePluginSpec(specString);
validatePackageName(spec.packageName);
this.ensurePackageJson();
await this.ensurePackageJson();
if (options.dryRun) {
return {
@@ -165,13 +169,17 @@ export class PluginManager {
// Resolve actual package name (strip version specifier)
const actualName = extractPackageName(spec.packageName);
const pkgPath = join(getPluginsNodeModules(), actualName, "package.json");
const pkgPath = path.join(getPluginsNodeModules(), actualName, "package.json");
if (!existsSync(pkgPath)) {
throw new Error(`Package installed but package.json not found at ${pkgPath}`);
let pkg: { name: string; version: string; omp?: PluginManifest; pi?: PluginManifest };
try {
pkg = await Bun.file(pkgPath).json();
} catch (err) {
if (isEnoent(err)) {
throw new Error(`Package installed but package.json not found at ${pkgPath}`);
}
throw err;
}
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
const manifest: PluginManifest = pkg.omp || pkg.pi || { version: pkg.version };
manifest.version = pkg.version;
@@ -201,17 +209,18 @@ export class PluginManager {
// null = use defaults
// Update runtime config
this.runtimeConfig.plugins[pkg.name] = {
const config = await this.ensureConfigLoaded();
config.plugins[pkg.name] = {
version: pkg.version,
enabledFeatures,
enabled: true,
};
this.saveRuntimeConfig();
await this.saveRuntimeConfig();
return {
name: pkg.name,
version: pkg.version,
path: join(getPluginsNodeModules(), actualName),
path: path.join(getPluginsNodeModules(), actualName),
manifest,
enabledFeatures,
enabled: true,
@@ -223,7 +232,7 @@ export class PluginManager {
*/
async uninstall(name: string): Promise<void> {
validatePackageName(name);
this.ensurePackageJson();
await this.ensurePackageJson();
const proc = Bun.spawn(["npm", "uninstall", name], {
cwd: getPluginsDir(),
@@ -238,9 +247,10 @@ export class PluginManager {
}
// Remove from runtime config
delete this.runtimeConfig.plugins[name];
delete this.runtimeConfig.settings[name];
this.saveRuntimeConfig();
const config = await this.ensureConfigLoaded();
delete config.plugins[name];
delete config.settings[name];
await this.saveRuntimeConfig();
}
/**
@@ -248,41 +258,49 @@ export class PluginManager {
*/
async list(): Promise<InstalledPlugin[]> {
const pkgJsonPath = getPluginsPackageJson();
if (!existsSync(pkgJsonPath)) {
return [];
let pkg: { dependencies?: Record<string, string> };
try {
pkg = await Bun.file(pkgJsonPath).json();
} catch (err) {
if (isEnoent(err)) return [];
throw err;
}
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
const deps = pkg.dependencies || {};
const projectOverrides = this.loadProjectOverrides();
const projectOverrides = await this.loadProjectOverrides();
const config = await this.ensureConfigLoaded();
const plugins: InstalledPlugin[] = [];
for (const [name] of Object.entries(deps)) {
const pluginPkgPath = join(getPluginsNodeModules(), name, "package.json");
if (existsSync(pluginPkgPath)) {
const pluginPkg = JSON.parse(readFileSync(pluginPkgPath, "utf-8"));
const manifest: PluginManifest = pluginPkg.omp || pluginPkg.pi || { version: pluginPkg.version };
manifest.version = pluginPkg.version;
const runtimeState = this.runtimeConfig.plugins[name] || {
version: pluginPkg.version,
enabledFeatures: null,
enabled: true,
};
// Apply project overrides
const isDisabledInProject = projectOverrides.disabled?.includes(name) ?? false;
const projectFeatures = projectOverrides.features?.[name];
plugins.push({
name,
version: pluginPkg.version,
path: join(getPluginsNodeModules(), name),
manifest,
enabledFeatures: projectFeatures ?? runtimeState.enabledFeatures,
enabled: runtimeState.enabled && !isDisabledInProject,
});
const pluginPkgPath = path.join(getPluginsNodeModules(), name, "package.json");
let pluginPkg: { version: string; omp?: PluginManifest; pi?: PluginManifest };
try {
pluginPkg = await Bun.file(pluginPkgPath).json();
} catch (err) {
if (isEnoent(err)) continue;
throw err;
}
const manifest: PluginManifest = pluginPkg.omp || pluginPkg.pi || { version: pluginPkg.version };
manifest.version = pluginPkg.version;
const runtimeState = config.plugins[name] || {
version: pluginPkg.version,
enabledFeatures: null,
enabled: true,
};
// Apply project overrides
const isDisabledInProject = projectOverrides.disabled?.includes(name) ?? false;
const projectFeatures = projectOverrides.features?.[name];
plugins.push({
name,
version: pluginPkg.version,
path: path.join(getPluginsNodeModules(), name),
manifest,
enabledFeatures: projectFeatures ?? runtimeState.enabledFeatures,
enabled: runtimeState.enabled && !isDisabledInProject,
});
}
return plugins;
@@ -292,52 +310,53 @@ export class PluginManager {
* Link a local plugin for development.
*/
async link(localPath: string): Promise<InstalledPlugin> {
const absolutePath = resolve(this.cwd, localPath);
const absolutePath = path.resolve(this.cwd, localPath);
const pkgFile = join(absolutePath, "package.json");
if (!existsSync(pkgFile)) {
throw new Error(`package.json not found at ${absolutePath}`);
const pkgFilePath = path.join(absolutePath, "package.json");
let pkg: { name?: string; version: string; omp?: PluginManifest; pi?: PluginManifest };
try {
pkg = await Bun.file(pkgFilePath).json();
} catch (err) {
if (isEnoent(err)) throw new Error(`package.json not found at ${absolutePath}`);
throw err;
}
const pkg = JSON.parse(readFileSync(pkgFile, "utf-8"));
if (!pkg.name) {
throw new Error("package.json must have a name field");
}
this.ensurePluginsDir();
await this.ensurePluginsDir();
const linkPath = join(getPluginsNodeModules(), pkg.name);
const linkPath = path.join(getPluginsNodeModules(), pkg.name);
// Handle scoped packages
if (pkg.name.startsWith("@")) {
const scopeDir = join(getPluginsNodeModules(), pkg.name.split("/")[0]);
if (!existsSync(scopeDir)) {
mkdirSync(scopeDir, { recursive: true });
}
const scopeDir = path.join(getPluginsNodeModules(), pkg.name.split("/")[0]);
await fs.promises.mkdir(scopeDir, { recursive: true });
}
// Remove existing
try {
const stat = lstatSync(linkPath);
if (stat.isSymbolicLink() || stat.isDirectory()) {
unlinkSync(linkPath);
const stats = await fs.promises.lstat(linkPath);
if (stats.isSymbolicLink() || stats.isDirectory()) {
await fs.promises.unlink(linkPath);
}
} catch {
// Doesn't exist
} catch (err) {
if (!isEnoent(err)) throw err;
}
symlinkSync(absolutePath, linkPath);
await fs.promises.symlink(absolutePath, linkPath);
const manifest: PluginManifest = pkg.omp || pkg.pi || { version: pkg.version };
manifest.version = pkg.version;
// Add to runtime config
this.runtimeConfig.plugins[pkg.name] = {
const config = await this.ensureConfigLoaded();
config.plugins[pkg.name] = {
version: pkg.version,
enabledFeatures: null,
enabled: true,
};
this.saveRuntimeConfig();
await this.saveRuntimeConfig();
return {
name: pkg.name,
@@ -357,11 +376,12 @@ export class PluginManager {
* Enable or disable a plugin globally.
*/
async setEnabled(name: string, enabled: boolean): Promise<void> {
if (!this.runtimeConfig.plugins[name]) {
const config = await this.ensureConfigLoaded();
if (!config.plugins[name]) {
throw new Error(`Plugin ${name} not found in runtime config`);
}
this.runtimeConfig.plugins[name].enabled = enabled;
this.saveRuntimeConfig();
config.plugins[name].enabled = enabled;
await this.saveRuntimeConfig();
}
// ==========================================================================
@@ -371,15 +391,17 @@ export class PluginManager {
/**
* Get enabled features for a plugin.
*/
getEnabledFeatures(name: string): string[] | null {
return this.runtimeConfig.plugins[name]?.enabledFeatures ?? null;
async getEnabledFeatures(name: string): Promise<string[] | null> {
const config = await this.ensureConfigLoaded();
return config.plugins[name]?.enabledFeatures ?? null;
}
/**
* Set enabled features for a plugin.
*/
async setEnabledFeatures(name: string, features: string[] | null): Promise<void> {
if (!this.runtimeConfig.plugins[name]) {
const config = await this.ensureConfigLoaded();
if (!config.plugins[name]) {
throw new Error(`Plugin ${name} not found in runtime config`);
}
@@ -398,8 +420,8 @@ export class PluginManager {
}
}
this.runtimeConfig.plugins[name].enabledFeatures = features;
this.saveRuntimeConfig();
config.plugins[name].enabledFeatures = features;
await this.saveRuntimeConfig();
}
// ==========================================================================
@@ -409,9 +431,10 @@ export class PluginManager {
/**
* Get all settings for a plugin.
*/
getPluginSettings(name: string): Record<string, unknown> {
const global = this.runtimeConfig.settings[name] || {};
const projectOverrides = this.loadProjectOverrides();
async getPluginSettings(name: string): Promise<Record<string, unknown>> {
const config = await this.ensureConfigLoaded();
const global = config.settings[name] || {};
const projectOverrides = await this.loadProjectOverrides();
const project = projectOverrides.settings?.[name] || {};
// Project settings override global
@@ -421,21 +444,23 @@ export class PluginManager {
/**
* Set a plugin setting value.
*/
setPluginSetting(name: string, key: string, value: unknown): void {
if (!this.runtimeConfig.settings[name]) {
this.runtimeConfig.settings[name] = {};
async setPluginSetting(name: string, key: string, value: unknown): Promise<void> {
const config = await this.ensureConfigLoaded();
if (!config.settings[name]) {
config.settings[name] = {};
}
this.runtimeConfig.settings[name][key] = value;
this.saveRuntimeConfig();
config.settings[name][key] = value;
await this.saveRuntimeConfig();
}
/**
* Delete a plugin setting.
*/
deletePluginSetting(name: string, key: string): void {
if (this.runtimeConfig.settings[name]) {
delete this.runtimeConfig.settings[name][key];
this.saveRuntimeConfig();
async deletePluginSetting(name: string, key: string): Promise<void> {
const config = await this.ensureConfigLoaded();
if (config.settings[name]) {
delete config.settings[name][key];
await this.saveRuntimeConfig();
}
}
@@ -451,15 +476,27 @@ export class PluginManager {
// Check 1: Plugins directory exists
const pluginsDir = getPluginsDir();
const pluginsDirExists = fs.existsSync(pluginsDir);
checks.push({
name: "plugins_directory",
status: existsSync(pluginsDir) ? "ok" : "warning",
message: existsSync(pluginsDir) ? `Found at ${pluginsDir}` : "Not created yet",
status: pluginsDirExists ? "ok" : "warning",
message: pluginsDirExists ? `Found at ${pluginsDir}` : "Not created yet",
});
// Check 2: package.json exists
const pkgJsonPath = getPluginsPackageJson();
const hasPkgJson = existsSync(pkgJsonPath);
let pkg: { dependencies?: Record<string, string> };
let hasPkgJson = true;
try {
pkg = await Bun.file(pkgJsonPath).json();
} catch (err) {
if (isEnoent(err)) {
hasPkgJson = false;
pkg = {};
} else {
throw err;
}
}
checks.push({
name: "package_manifest",
status: hasPkgJson ? "ok" : "warning",
@@ -468,7 +505,7 @@ export class PluginManager {
// Check 3: node_modules exists
const nodeModulesPath = getPluginsNodeModules();
const hasNodeModules = existsSync(nodeModulesPath);
const hasNodeModules = fs.existsSync(nodeModulesPath);
checks.push({
name: "node_modules",
status: hasNodeModules ? "ok" : hasPkgJson ? "error" : "warning",
@@ -478,36 +515,37 @@ export class PluginManager {
if (!hasPkgJson) {
return checks;
}
// Check each installed plugin
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
const deps = pkg.dependencies || {};
const config = await this.ensureConfigLoaded();
for (const [name] of Object.entries(deps)) {
const pluginPath = join(nodeModulesPath, name);
const pluginPkgPath = join(pluginPath, "package.json");
const pluginPath = path.join(nodeModulesPath, name);
const pluginPkgPath = path.join(pluginPath, "package.json");
if (!existsSync(pluginPath)) {
const fixed = options.fix ? await this.fixMissingPlugin() : false;
checks.push({
name: `plugin:${name}`,
status: "error",
message: "Missing from node_modules",
fixed,
});
continue;
let pluginPkg: { version: string; description?: string; omp?: PluginManifest; pi?: PluginManifest };
try {
pluginPkg = await Bun.file(pluginPkgPath).json();
} catch (err) {
if (isEnoent(err)) {
if (!fs.existsSync(pluginPath)) {
const fixed = options.fix ? await this.fixMissingPlugin() : false;
checks.push({
name: `plugin:${name}`,
status: "error",
message: "Missing from node_modules",
fixed,
});
} else {
checks.push({
name: `plugin:${name}`,
status: "error",
message: "Missing package.json",
});
}
continue;
}
throw err;
}
if (!existsSync(pluginPkgPath)) {
checks.push({
name: `plugin:${name}`,
status: "error",
message: "Missing package.json",
});
continue;
}
const pluginPkg = JSON.parse(readFileSync(pluginPkgPath, "utf-8"));
const hasManifest = !!(pluginPkg.omp || pluginPkg.pi);
const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;
@@ -521,8 +559,8 @@ export class PluginManager {
// Check tools path exists if specified
if (manifest?.tools) {
const toolsPath = join(pluginPath, manifest.tools);
if (!existsSync(toolsPath)) {
const toolsPath = path.join(pluginPath, manifest.tools);
if (!fs.existsSync(toolsPath)) {
checks.push({
name: `plugin:${name}:tools`,
status: "error",
@@ -533,8 +571,8 @@ export class PluginManager {
// Check hooks path exists if specified
if (manifest?.hooks) {
const hooksPath = join(pluginPath, manifest.hooks);
if (!existsSync(hooksPath)) {
const hooksPath = path.join(pluginPath, manifest.hooks);
if (!fs.existsSync(hooksPath)) {
checks.push({
name: `plugin:${name}:hooks`,
status: "error",
@@ -544,11 +582,11 @@ export class PluginManager {
}
// Check enabled features exist in manifest
const runtimeState = this.runtimeConfig.plugins[name];
const runtimeState = config.plugins[name];
if (runtimeState?.enabledFeatures && manifest?.features) {
for (const feat of runtimeState.enabledFeatures) {
if (!(feat in manifest.features)) {
const fixed = options.fix ? this.removeInvalidFeature(name, feat) : false;
const fixed = options.fix ? await this.removeInvalidFeature(name, feat) : false;
checks.push({
name: `plugin:${name}:feature:${feat}`,
status: "warning",
@@ -561,9 +599,9 @@ export class PluginManager {
}
// Check for orphaned runtime config entries
for (const name of Object.keys(this.runtimeConfig.plugins)) {
for (const name of Object.keys(config.plugins)) {
if (!(name in deps)) {
const fixed = options.fix ? this.removeOrphanedConfig(name) : false;
const fixed = options.fix ? await this.removeOrphanedConfig(name) : false;
checks.push({
name: `orphan:${name}`,
status: "warning",
@@ -590,20 +628,22 @@ export class PluginManager {
}
}
private removeInvalidFeature(name: string, feat: string): boolean {
const state = this.runtimeConfig.plugins[name];
private async removeInvalidFeature(name: string, feat: string): Promise<boolean> {
const config = await this.ensureConfigLoaded();
const state = config.plugins[name];
if (state?.enabledFeatures) {
state.enabledFeatures = state.enabledFeatures.filter((f) => f !== feat);
this.saveRuntimeConfig();
await this.saveRuntimeConfig();
return true;
}
return false;
}
private removeOrphanedConfig(name: string): boolean {
delete this.runtimeConfig.plugins[name];
delete this.runtimeConfig.settings[name];
this.saveRuntimeConfig();
private async removeOrphanedConfig(name: string): Promise<boolean> {
const config = await this.ensureConfigLoaded();
delete config.plugins[name];
delete config.settings[name];
await this.saveRuntimeConfig();
return true;
}
}
@@ -1,5 +1,5 @@
import { homedir } from "node:os";
import { join } from "node:path";
import * as path from "node:path";
import { CONFIG_DIR_NAME, getConfigDirPaths } from "../../config";
// =============================================================================
@@ -8,27 +8,27 @@ import { CONFIG_DIR_NAME, getConfigDirPaths } from "../../config";
/** Root plugin directory: ~/.omp/plugins (not under agent/) */
export function getPluginsDir(): string {
return join(homedir(), CONFIG_DIR_NAME, "plugins");
return path.join(homedir(), CONFIG_DIR_NAME, "plugins");
}
/** Where npm installs packages: ~/.omp/plugins/node_modules */
export function getPluginsNodeModules(): string {
return join(getPluginsDir(), "node_modules");
return path.join(getPluginsDir(), "node_modules");
}
/** Plugin manifest: ~/.omp/plugins/package.json */
export function getPluginsPackageJson(): string {
return join(getPluginsDir(), "package.json");
return path.join(getPluginsDir(), "package.json");
}
/** Plugin lock file: ~/.omp/plugins/omp-plugins.lock.json */
export function getPluginsLockfile(): string {
return join(getPluginsDir(), "omp-plugins.lock.json");
return path.join(getPluginsDir(), "omp-plugins.lock.json");
}
/** Project-local plugin overrides: .omp/plugin-overrides.json (primary) */
export function getProjectPluginOverrides(cwd: string): string {
return join(cwd, CONFIG_DIR_NAME, "plugin-overrides.json");
return path.join(cwd, CONFIG_DIR_NAME, "plugin-overrides.json");
}
/** All possible project plugin override paths (primary + legacy) */
@@ -1,6 +1,5 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { realpath } from "node:fs/promises";
import { basename, join } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { skillCapability } from "../capability/skill";
import type { SourceMeta } from "../capability/types";
@@ -44,15 +43,15 @@ export interface LoadSkillsFromDirOptions {
* Skills are directories containing a SKILL.md file with frontmatter including a description.
* @deprecated Use loadSync("skills") from discovery API instead
*/
export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {
export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Promise<LoadSkillsResult> {
const skills: Skill[] = [];
const warnings: SkillWarning[] = [];
const seenPaths = new Set<string>();
function addSkill(skillFile: string, skillDir: string, dirName: string) {
async function addSkill(skillFile: string, skillDir: string, dirName: string): Promise<void> {
if (seenPaths.has(skillFile)) return;
try {
const content = readFileSync(skillFile, "utf-8");
const content = await fs.readFile(skillFile, "utf-8");
const { frontmatter } = parseFrontmatter(content, { source: skillFile });
const name = (frontmatter.name as string) || dirName;
const description = frontmatter.description as string;
@@ -72,34 +71,42 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
}
}
function scanDir(dir: string) {
async function scanDir(dir: string): Promise<void> {
try {
const entries = readdirSync(dir, { withFileTypes: true });
const entries = await fs.readdir(dir, { withFileTypes: true });
const tasks: Promise<void>[] = [];
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
const fullPath = join(dir, entry.name);
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const skillFile = join(fullPath, "SKILL.md");
try {
const stat = statSync(skillFile);
if (stat.isFile()) {
addSkill(skillFile, fullPath, entry.name);
}
} catch {
// No SKILL.md in this directory
}
scanDir(fullPath);
const skillFile = path.join(fullPath, "SKILL.md");
tasks.push(
fs
.stat(skillFile)
.then((s) => {
if (s.isFile()) {
return addSkill(skillFile, fullPath, entry.name);
}
})
.catch(() => {
// No SKILL.md in this directory
}),
);
tasks.push(scanDir(fullPath));
} else if (entry.isFile() && entry.name === "SKILL.md") {
addSkill(fullPath, dir, basename(dir));
tasks.push(addSkill(fullPath, dir, path.basename(dir)));
}
}
await Promise.all(tasks);
} catch (err) {
warnings.push({ skillPath: dir, message: `Failed to read directory: ${err}` });
}
}
scanDir(options.dir);
await scanDir(options.dir);
return { skills, warnings };
}
@@ -108,15 +115,15 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
* Scan a directory for SKILL.md files recursively.
* Used internally by loadSkills for custom directories.
*/
function scanDirectoryForSkills(dir: string): LoadSkillsResult {
async function scanDirectoryForSkills(dir: string): Promise<LoadSkillsResult> {
const skills: Skill[] = [];
const warnings: SkillWarning[] = [];
const seenPaths = new Set<string>();
function addSkill(skillFile: string, skillDir: string, dirName: string) {
async function addSkill(skillFile: string, skillDir: string, dirName: string): Promise<void> {
if (seenPaths.has(skillFile)) return;
try {
const content = readFileSync(skillFile, "utf-8");
const content = await fs.readFile(skillFile, "utf-8");
const { frontmatter } = parseFrontmatter(content, { source: skillFile });
const name = (frontmatter.name as string) || dirName;
const description = frontmatter.description as string;
@@ -136,34 +143,42 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
}
}
function scanDir(currentDir: string) {
async function scanDir(currentDir: string): Promise<void> {
try {
const entries = readdirSync(currentDir, { withFileTypes: true });
const entries = await fs.readdir(currentDir, { withFileTypes: true });
const tasks: Promise<void>[] = [];
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
const fullPath = join(currentDir, entry.name);
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
const skillFile = join(fullPath, "SKILL.md");
try {
const stat = statSync(skillFile);
if (stat.isFile()) {
addSkill(skillFile, fullPath, entry.name);
}
} catch {
// No SKILL.md in this directory
}
scanDir(fullPath);
const skillFile = path.join(fullPath, "SKILL.md");
tasks.push(
fs
.stat(skillFile)
.then((s) => {
if (s.isFile()) {
return addSkill(skillFile, fullPath, entry.name);
}
})
.catch(() => {
// No SKILL.md in this directory
}),
);
tasks.push(scanDir(fullPath));
} else if (entry.isFile() && entry.name === "SKILL.md") {
addSkill(fullPath, currentDir, basename(currentDir));
tasks.push(addSkill(fullPath, currentDir, path.basename(currentDir)));
}
}
await Promise.all(tasks);
} catch (err) {
warnings.push({ skillPath: currentDir, message: `Failed to read directory: ${err}` });
}
}
scanDir(dir);
await scanDir(dir);
return { skills, warnings };
}
@@ -239,7 +254,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
const realPaths = await Promise.all(
filteredSkills.map(async (capSkill) => {
try {
return await realpath(capSkill.path);
return await fs.realpath(capSkill.path);
} catch {
return capSkill.path;
}
@@ -279,8 +294,8 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
// Process custom directories - scan directly without using full provider system
const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
for (const dir of customDirectories) {
const customSkills = scanDirectoryForSkills(dir);
const customScanResults = await Promise.all(customDirectories.map((dir) => scanDirectoryForSkills(dir)));
for (const customSkills of customScanResults) {
for (const s of customSkills.skills) {
if (matchesIgnorePatterns(s.name)) continue;
if (!matchesIncludePatterns(s.name)) continue;
@@ -303,7 +318,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
const customRealPaths = await Promise.all(
allCustomSkills.map(async ({ path }) => {
try {
return await realpath(path);
return await fs.realpath(path);
} catch {
return path;
}
@@ -9,8 +9,9 @@
* - agent://<id>?q=<query> - JSON extraction via query form
*/
import * as fs from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { applyQuery, pathToQuery } from "./json-query";
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
@@ -25,9 +26,9 @@ export interface AgentProtocolOptions {
/**
* List available output IDs in artifacts directory.
*/
function listAvailableOutputs(artifactsDir: string): string[] {
async function listAvailableOutputs(artifactsDir: string): Promise<string[]> {
try {
const files = fs.readdirSync(artifactsDir);
const files = await fs.readdir(artifactsDir);
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
} catch {
return [];
@@ -51,8 +52,13 @@ export class AgentProtocolHandler implements ProtocolHandler {
throw new Error("No session - agent outputs unavailable");
}
if (!fs.existsSync(artifactsDir)) {
throw new Error("No artifacts directory found");
try {
await fs.stat(artifactsDir);
} catch (err) {
if (isEnoent(err)) {
throw new Error("No artifacts directory found");
}
throw err;
}
// Extract output ID from host
@@ -73,10 +79,15 @@ export class AgentProtocolHandler implements ProtocolHandler {
// Load the output file
const outputPath = path.join(artifactsDir, `${outputId}.md`);
if (!fs.existsSync(outputPath)) {
const available = listAvailableOutputs(artifactsDir);
const availableStr = available.length > 0 ? available.join(", ") : "none";
throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
try {
await fs.stat(outputPath);
} catch (err) {
if (isEnoent(err)) {
const available = await listAvailableOutputs(artifactsDir);
const availableStr = available.length > 0 ? available.join(", ") : "none";
throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
}
throw err;
}
const rawContent = await Bun.file(outputPath).text();
@@ -10,8 +10,9 @@
* Pagination is handled by the read tool via offset/limit parameters.
*/
import * as fs from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
export interface ArtifactProtocolOptions {
@@ -24,9 +25,9 @@ export interface ArtifactProtocolOptions {
/**
* List available artifact IDs in the directory.
*/
function listAvailableArtifacts(artifactsDir: string): string[] {
async function listAvailableArtifacts(artifactsDir: string): Promise<string[]> {
try {
const files = fs.readdirSync(artifactsDir);
const files = await fs.readdir(artifactsDir);
return files
.filter((f) => /^\d+\./.test(f))
.map((f) => f.split(".")[0])
@@ -64,17 +65,21 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
throw new Error(`artifact:// ID must be numeric, got: ${id}`);
}
// Check directory exists
if (!fs.existsSync(artifactsDir)) {
throw new Error("No artifacts directory found");
// Check directory exists and find file matching ID prefix
let files: string[];
try {
files = await fs.readdir(artifactsDir);
} catch (err) {
if (isEnoent(err)) {
throw new Error("No artifacts directory found");
}
throw err;
}
// Find file matching ID prefix
const files = fs.readdirSync(artifactsDir);
const match = files.find((f) => f.startsWith(`${id}.`));
if (!match) {
const available = listAvailableArtifacts(artifactsDir);
const available = await listAvailableArtifacts(artifactsDir);
const availableStr = available.length > 0 ? available.join(", ") : "none";
throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
}
@@ -1,19 +1,9 @@
import {
closeSync,
existsSync,
mkdirSync,
openSync,
readFileSync,
renameSync,
statSync,
unlinkSync,
utimesSync,
writeFileSync,
} from "node:fs";
import * as fs from "node:fs";
import { createServer } from "node:net";
import { delimiter, join } from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import type { Subprocess } from "bun";
import { getAgentDir } from "../config";
import { getShellConfig, killProcessTree } from "../utils/shell";
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
@@ -175,7 +165,7 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
async function resolveVenvPath(cwd: string): Promise<string | null> {
if (process.env.VIRTUAL_ENV) return process.env.VIRTUAL_ENV;
const candidates = [join(cwd, ".venv"), join(cwd, "venv")];
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
for (const candidate of candidates) {
if (await Bun.file(candidate).exists()) {
return candidate;
@@ -189,12 +179,12 @@ async function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
const venvPath = env.VIRTUAL_ENV ?? (await resolveVenvPath(cwd));
if (venvPath) {
env.VIRTUAL_ENV = venvPath;
const binDir = process.platform === "win32" ? join(venvPath, "Scripts") : join(venvPath, "bin");
const pythonCandidate = join(binDir, process.platform === "win32" ? "python.exe" : "python");
const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
if (await Bun.file(pythonCandidate).exists()) {
const pathKey = resolvePathKey(env);
const currentPath = env[pathKey];
env[pathKey] = currentPath ? `${binDir}${delimiter}${currentPath}` : binDir;
env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
return { pythonPath: pythonCandidate, env, venvPath };
}
}
@@ -232,33 +222,29 @@ async function allocatePort(): Promise<number> {
}
function getGatewayDir(): string {
return join(getAgentDir(), GATEWAY_DIR_NAME);
return path.join(getAgentDir(), GATEWAY_DIR_NAME);
}
function getGatewayInfoPath(): string {
return join(getGatewayDir(), GATEWAY_INFO_FILE);
return path.join(getGatewayDir(), GATEWAY_INFO_FILE);
}
function getGatewayLockPath(): string {
return join(getGatewayDir(), GATEWAY_LOCK_FILE);
return path.join(getGatewayDir(), GATEWAY_LOCK_FILE);
}
function writeLockInfo(lockPath: string, fd: number): void {
async function writeLockInfo(lockPath: string): Promise<void> {
const payload: GatewayLockInfo = { pid: process.pid, startedAt: Date.now() };
try {
writeFileSync(fd, JSON.stringify(payload));
await Bun.write(lockPath, JSON.stringify(payload));
} catch {
try {
writeFileSync(lockPath, JSON.stringify(payload));
} catch {
// Ignore lock write failures
}
// Ignore lock write failures
}
}
function readLockInfo(lockPath: string): GatewayLockInfo | null {
async function readLockInfo(lockPath: string): Promise<GatewayLockInfo | null> {
try {
const raw = readFileSync(lockPath, "utf-8");
const raw = await Bun.file(lockPath).text();
const parsed = JSON.parse(raw) as Partial<GatewayLockInfo>;
if (typeof parsed.pid === "number" && Number.isFinite(parsed.pid)) {
return { pid: parsed.pid, startedAt: typeof parsed.startedAt === "number" ? parsed.startedAt : 0 };
@@ -269,36 +255,41 @@ function readLockInfo(lockPath: string): GatewayLockInfo | null {
return null;
}
function ensureGatewayDir(): void {
async function ensureGatewayDir(): Promise<void> {
const dir = getGatewayDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
await fs.promises.mkdir(dir, { recursive: true });
}
async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
ensureGatewayDir();
await ensureGatewayDir();
const lockPath = getGatewayLockPath();
const start = Date.now();
while (true) {
let fd: fs.promises.FileHandle | undefined;
try {
const fd = openSync(lockPath, "wx");
const heartbeat = setInterval(() => {
try {
const now = new Date();
utimesSync(lockPath, now, now);
} catch {
// Ignore heartbeat errors
fd = await fs.promises.open(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL);
let heartbeatRunning = true;
const heartbeat = (async () => {
while (heartbeatRunning) {
await Bun.sleep(GATEWAY_LOCK_HEARTBEAT_MS);
if (!heartbeatRunning) break;
try {
const now = new Date();
await fs.promises.utimes(lockPath, now, now);
} catch {
// Ignore heartbeat errors
}
}
}, GATEWAY_LOCK_HEARTBEAT_MS);
})();
try {
writeLockInfo(lockPath, fd);
await writeLockInfo(lockPath);
return await handler();
} finally {
clearInterval(heartbeat);
heartbeatRunning = false;
void heartbeat.catch(() => {}); // Don't await - let it die naturally
try {
closeSync(fd);
unlinkSync(lockPath);
await fd.close();
await fs.promises.unlink(lockPath);
} catch {
// Ignore lock cleanup errors
}
@@ -308,15 +299,15 @@ async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
if (error.code === "EEXIST") {
let removedStale = false;
try {
const stat = statSync(lockPath);
const lockInfo = readLockInfo(lockPath);
const lockStat = await fs.promises.stat(lockPath);
const lockInfo = await readLockInfo(lockPath);
const lockPid = lockInfo?.pid;
const lockAgeMs = lockInfo?.startedAt ? Date.now() - lockInfo.startedAt : Date.now() - stat.mtimeMs;
const lockAgeMs = lockInfo?.startedAt ? Date.now() - lockInfo.startedAt : Date.now() - lockStat.mtimeMs;
const staleByTime = lockAgeMs > GATEWAY_LOCK_STALE_MS;
const staleByPid = lockPid !== undefined && !isPidRunning(lockPid);
const staleByMissingPid = lockPid === undefined && staleByTime;
if (staleByPid || staleByMissingPid) {
unlinkSync(lockPath);
await fs.promises.unlink(lockPath);
removedStale = true;
logger.warn("Removed stale shared gateway lock", { path: lockPath, pid: lockPid });
}
@@ -336,14 +327,12 @@ async function withGatewayLock<T>(handler: () => Promise<T>): Promise<T> {
}
}
function readGatewayInfo(): GatewayInfo | null {
async function readGatewayInfo(): Promise<GatewayInfo | null> {
const infoPath = getGatewayInfoPath();
if (!existsSync(infoPath)) return null;
try {
const content = readFileSync(infoPath, "utf-8");
const content = await Bun.file(infoPath).text();
const parsed = JSON.parse(content) as Partial<GatewayInfo>;
if (!parsed || typeof parsed !== "object") return null;
if (typeof parsed.url !== "string" || typeof parsed.pid !== "number" || typeof parsed.startedAt !== "number") {
return null;
}
@@ -354,26 +343,25 @@ function readGatewayInfo(): GatewayInfo | null {
pythonPath: typeof parsed.pythonPath === "string" ? parsed.pythonPath : undefined,
venvPath: typeof parsed.venvPath === "string" || parsed.venvPath === null ? parsed.venvPath : undefined,
};
} catch {
} catch (err) {
if (isEnoent(err)) return null;
return null;
}
}
function writeGatewayInfo(info: GatewayInfo): void {
async function writeGatewayInfo(info: GatewayInfo): Promise<void> {
const infoPath = getGatewayInfoPath();
const tempPath = `${infoPath}.tmp`;
writeFileSync(tempPath, JSON.stringify(info, null, 2));
renameSync(tempPath, infoPath);
await Bun.write(tempPath, JSON.stringify(info, null, 2));
await fs.promises.rename(tempPath, infoPath);
}
function clearGatewayInfo(): void {
async function clearGatewayInfo(): Promise<void> {
const infoPath = getGatewayInfoPath();
if (existsSync(infoPath)) {
try {
unlinkSync(infoPath);
} catch {
// Ignore errors on cleanup
}
try {
await fs.promises.unlink(infoPath);
} catch {
// Ignore errors on cleanup (file may not exist)
}
}
@@ -388,12 +376,9 @@ function isPidRunning(pid: number): boolean {
async function isGatewayHealthy(url: string): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
const response = await fetch(`${url}/api/kernelspecs`, {
signal: controller.signal,
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
});
clearTimeout(timeout);
return response.ok;
} catch {
return false;
@@ -497,7 +482,7 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
try {
return await withGatewayLock(async () => {
const existingInfo = readGatewayInfo();
const existingInfo = await readGatewayInfo();
if (existingInfo) {
if (await isGatewayAlive(existingInfo)) {
localGatewayUrl = existingInfo.url;
@@ -510,7 +495,7 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
if (isPidRunning(existingInfo.pid)) {
await killGateway(existingInfo.pid, "stale");
}
clearGatewayInfo();
await clearGatewayInfo();
}
const { url, pid, pythonPath, venvPath } = await startGatewayProcess(cwd);
@@ -521,7 +506,7 @@ export async function acquireSharedGateway(cwd: string): Promise<AcquireResult |
pythonPath,
venvPath,
};
writeGatewayInfo(info);
await writeGatewayInfo(info);
isCoordinatorInitialized = true;
logger.debug("Started global Python gateway", { url, pid });
return { url, isShared: true };
@@ -538,13 +523,13 @@ export async function releaseSharedGateway(): Promise<void> {
if (!isCoordinatorInitialized) return;
}
export function getSharedGatewayUrl(): string | null {
export async function getSharedGatewayUrl(): Promise<string | null> {
if (localGatewayUrl) return localGatewayUrl;
return readGatewayInfo()?.url ?? null;
return (await readGatewayInfo())?.url ?? null;
}
export function isSharedGatewayActive(): boolean {
return getGatewayStatus().active;
export async function isSharedGatewayActive(): Promise<boolean> {
return (await getGatewayStatus()).active;
}
export interface GatewayStatus {
@@ -556,8 +541,8 @@ export interface GatewayStatus {
venvPath: string | null;
}
export function getGatewayStatus(): GatewayStatus {
const info = readGatewayInfo();
export async function getGatewayStatus(): Promise<GatewayStatus> {
const info = await readGatewayInfo();
if (!info) {
return {
active: false,
@@ -582,12 +567,12 @@ export function getGatewayStatus(): GatewayStatus {
export async function shutdownSharedGateway(): Promise<void> {
try {
await withGatewayLock(async () => {
const info = readGatewayInfo();
const info = await readGatewayInfo();
if (!info) return;
if (isPidRunning(info.pid)) {
await killGateway(info.pid, "shutdown");
}
clearGatewayInfo();
await clearGatewayInfo();
});
} catch (err) {
logger.warn("Failed to shutdown shared gateway", {
+9 -17
View File
@@ -1,5 +1,5 @@
import { createServer } from "node:net";
import { delimiter, join } from "node:path";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { $, type Subprocess } from "bun";
import { nanoid } from "nanoid";
@@ -239,7 +239,7 @@ function filterEnv(env: Record<string, string | undefined>): Record<string, stri
async function resolveVenvPath(cwd: string): Promise<string | null> {
if (process.env.VIRTUAL_ENV) return process.env.VIRTUAL_ENV;
const candidates = [join(cwd, ".venv"), join(cwd, "venv")];
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
for (const candidate of candidates) {
if (await Bun.file(candidate).exists()) {
return candidate;
@@ -253,12 +253,12 @@ async function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
const venvPath = env.VIRTUAL_ENV ?? (await resolveVenvPath(cwd));
if (venvPath) {
env.VIRTUAL_ENV = venvPath;
const binDir = process.platform === "win32" ? join(venvPath, "Scripts") : join(venvPath, "bin");
const pythonCandidate = join(binDir, process.platform === "win32" ? "python.exe" : "python");
const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
if (await Bun.file(pythonCandidate).exists()) {
const pathKey = resolvePathKey(env);
const currentPath = env[pathKey];
env[pathKey] = currentPath ? `${binDir}${delimiter}${currentPath}` : binDir;
env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
return { pythonPath: pythonCandidate, env };
}
}
@@ -309,13 +309,11 @@ async function checkExternalGatewayAvailability(config: ExternalGatewayConfig):
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`${config.url}/api/kernelspecs`, {
headers,
signal: controller.signal,
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
});
clearTimeout(timeout);
if (response.ok) {
return { ok: true };
@@ -627,7 +625,7 @@ export class PythonKernel {
OMP_SHELL_SNAPSHOT: snapshotPath ?? undefined,
};
const pythonPathParts = [options.cwd, kernelEnv.PYTHONPATH].filter(Boolean).join(delimiter);
const pythonPathParts = [options.cwd, kernelEnv.PYTHONPATH].filter(Boolean).join(path.delimiter);
if (pythonPathParts) {
kernelEnv.PYTHONPATH = pythonPathParts;
}
@@ -1064,14 +1062,11 @@ export class PythonKernel {
async interrupt(): Promise<void> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}/interrupt`, {
method: "POST",
headers: this.#authHeaders(),
signal: controller.signal,
signal: AbortSignal.timeout(2000),
});
clearTimeout(timeout);
} catch (err: unknown) {
logger.warn("Failed to interrupt kernel via API", { error: err instanceof Error ? err.message : String(err) });
}
@@ -1138,13 +1133,10 @@ export class PythonKernel {
async ping(timeoutMs: number = HEARTBEAT_TIMEOUT_MS): Promise<boolean> {
if (!this.isAlive()) return false;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
signal: controller.signal,
signal: AbortSignal.timeout(timeoutMs),
headers: this.#authHeaders(),
});
clearTimeout(timeout);
if (response.ok) {
this.#heartbeatFailures = 0;
return true;
+6 -6
View File
@@ -1,6 +1,6 @@
import { readdir } from "node:fs/promises";
import * as fs from "node:fs/promises";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import * as path from "node:path";
export type PythonModuleSource = "user" | "project";
@@ -38,12 +38,12 @@ interface ModuleCandidate {
async function listModuleCandidates(dir: string, source: PythonModuleSource): Promise<ModuleCandidate[]> {
try {
const entries = await readdir(dir, { withFileTypes: true });
const entries = await fs.readdir(dir, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".py"))
.map((entry) => ({
name: entry.name,
path: resolve(dir, entry.name),
path: path.resolve(dir, entry.name),
source,
}));
} catch {
@@ -68,8 +68,8 @@ export async function discoverPythonModules(options: DiscoverPythonModulesOption
const cwd = options.cwd ?? process.cwd();
const homeDir = options.homeDir ?? homedir();
const userDirs = [join(homeDir, ".omp", "agent", "modules"), join(homeDir, ".pi", "agent", "modules")];
const projectDirs = [resolve(cwd, ".omp", "modules"), resolve(cwd, ".pi", "modules")];
const userDirs = [path.join(homeDir, ".omp", "agent", "modules"), path.join(homeDir, ".pi", "agent", "modules")];
const projectDirs = [path.resolve(cwd, ".omp", "modules"), path.resolve(cwd, ".pi", "modules")];
const userCandidates = (await Promise.all(userDirs.map((dir) => listModuleCandidates(dir, "user")))).flat();
const projectCandidates = (await Promise.all(projectDirs.map((dir) => listModuleCandidates(dir, "project")))).flat();
+15 -4
View File
@@ -1,5 +1,4 @@
import * as fs from "node:fs";
import { logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
import { applyWorkspaceEdit } from "./edits";
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
@@ -512,7 +511,13 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
return;
}
const content = fs.readFileSync(filePath, "utf-8");
let content: string;
try {
content = await Bun.file(filePath).text();
} catch (err) {
if (isEnoent(err)) return;
throw err;
}
const languageId = detectLanguageId(filePath);
await sendNotification(client, "textDocument/didOpen", {
@@ -634,7 +639,13 @@ export async function refreshFile(client: LspClient, filePath: string): Promise<
return;
}
const content = fs.readFileSync(filePath, "utf-8");
let content: string;
try {
content = await Bun.file(filePath).text();
} catch (err) {
if (isEnoent(err)) return;
throw err;
}
const version = ++info.version;
await sendNotification(client, "textDocument/didChange", {
+11 -11
View File
@@ -1,5 +1,5 @@
import { homedir } from "node:os";
import { basename, extname, join } from "node:path";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
import { globSync } from "glob";
@@ -30,7 +30,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
}
function parseConfigContent(content: string, filePath: string): unknown {
const extension = extname(filePath).toLowerCase();
const extension = path.extname(filePath).toLowerCase();
if (extension === ".yaml" || extension === ".yml") {
return YAML.parse(content) as unknown;
}
@@ -160,7 +160,7 @@ export async function hasRootMarkers(cwd: string, markers: string[]): Promise<bo
// Handle glob-like patterns (e.g., "*.cabal")
if (marker.includes("*")) {
try {
const matches = globSync(join(cwd, marker));
const matches = globSync(path.join(cwd, marker));
if (matches.length > 0) {
return true;
}
@@ -169,7 +169,7 @@ export async function hasRootMarkers(cwd: string, markers: string[]): Promise<bo
}
continue;
}
const filePath = join(cwd, marker);
const filePath = path.join(cwd, marker);
if (await Bun.file(filePath).exists()) {
return true;
}
@@ -211,7 +211,7 @@ export async function resolveCommand(command: string, cwd: string): Promise<stri
// Check local bin directories based on project markers
for (const { markers, binDir } of LOCAL_BIN_PATHS) {
if (await hasRootMarkers(cwd, markers)) {
const localPath = join(cwd, binDir, command);
const localPath = path.join(cwd, binDir, command);
if (await Bun.file(localPath).exists()) {
return localPath;
}
@@ -232,14 +232,14 @@ function getConfigPaths(cwd: string): string[] {
// Project root files (highest priority)
for (const filename of filenames) {
paths.push(join(cwd, filename));
paths.push(path.join(cwd, filename));
}
// Project config directories (.omp/, .pi/, .claude/)
const projectDirs = getConfigDirPaths("", { user: false, project: true, cwd });
for (const dir of projectDirs) {
for (const filename of filenames) {
paths.push(join(dir, filename));
paths.push(path.join(dir, filename));
}
}
@@ -247,13 +247,13 @@ function getConfigPaths(cwd: string): string[] {
const userDirs = getConfigDirPaths("", { user: true, project: false });
for (const dir of userDirs) {
for (const filename of filenames) {
paths.push(join(dir, filename));
paths.push(path.join(dir, filename));
}
}
// User home root files (lowest priority fallback)
for (const filename of filenames) {
paths.push(join(homedir(), filename));
paths.push(path.join(homedir(), filename));
}
return paths;
@@ -352,8 +352,8 @@ export async function loadConfig(cwd: string): Promise<LspConfig> {
* Returns servers sorted with primary (non-linter) servers first.
*/
export function getServersForFile(config: LspConfig, filePath: string): Array<[string, ServerConfig]> {
const ext = extname(filePath).toLowerCase();
const fileName = basename(filePath).toLowerCase();
const ext = path.extname(filePath).toLowerCase();
const fileName = path.basename(filePath).toLowerCase();
const matches: Array<[string, ServerConfig]> = [];
for (const [name, serverConfig] of Object.entries(config.servers)) {
+5 -5
View File
@@ -1,4 +1,4 @@
import { mkdir, rename, rm } from "node:fs/promises";
import * as fs from "node:fs/promises";
import path from "node:path";
import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types";
import { uriToFile } from "./utils";
@@ -86,20 +86,20 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
if (change.kind === "create") {
const createOp = change as CreateFile;
const filePath = uriToFile(createOp.uri);
await mkdir(path.dirname(filePath), { recursive: true });
await fs.mkdir(path.dirname(filePath), { recursive: true });
await Bun.write(filePath, "");
applied.push(`Created ${path.relative(cwd, filePath)}`);
} else if (change.kind === "rename") {
const renameOp = change as RenameFile;
const oldPath = uriToFile(renameOp.oldUri);
const newPath = uriToFile(renameOp.newUri);
await mkdir(path.dirname(newPath), { recursive: true });
await rename(oldPath, newPath);
await fs.mkdir(path.dirname(newPath), { recursive: true });
await fs.rename(oldPath, newPath);
applied.push(`Renamed ${path.relative(cwd, oldPath)} → ${path.relative(cwd, newPath)}`);
} else if (change.kind === "delete") {
const deleteOp = change as DeleteFile;
const filePath = uriToFile(deleteOp.uri);
await rm(filePath, { recursive: true });
await fs.rm(filePath, { recursive: true });
applied.push(`Deleted ${path.relative(cwd, filePath)}`);
}
}
+24 -12
View File
@@ -1,8 +1,7 @@
import type { Dirent } from "node:fs";
import { existsSync, statSync } from "node:fs";
import * as fs from "node:fs";
import path from "node:path";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { logger, once, untilAborted } from "@oh-my-pi/pi-utils";
import { isEnoent, logger, once, untilAborted } from "@oh-my-pi/pi-utils";
import type { BunFile } from "bun";
import { renderPromptTemplate } from "../config/prompt-templates";
import { type Theme, theme } from "../modes/theme/theme";
@@ -257,18 +256,18 @@ function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: n
const normalized = extensions.map((ext) => ext.toLowerCase());
const search = (dir: string, depth: number): string | null => {
if (depth > maxDepth) return null;
const entries: Dirent[] = [];
const entries: fs.Dirent[] = [];
try {
const names = Array.from(new Bun.Glob("*").scanSync({ cwd: dir, onlyFiles: false }));
for (const name of names) {
const fullPath = path.join(dir, name);
let isDir = false;
try {
isDir = statSync(fullPath).isDirectory();
isDir = fs.statSync(fullPath).isDirectory();
} catch {
continue;
}
entries.push({ name, isFile: () => !isDir, isDirectory: () => isDir } as Dirent);
entries.push({ name, isFile: () => !isDir, isDirectory: () => isDir } as fs.Dirent);
}
} catch {
return null;
@@ -359,25 +358,38 @@ interface ProjectType {
description: string;
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.promises.stat(filePath);
return true;
} catch (err) {
if (isEnoent(err)) return false;
throw err;
}
}
/** Detect project type from root markers */
function detectProjectType(cwd: string): ProjectType {
async function detectProjectType(cwd: string): Promise<ProjectType> {
// Check for Rust (Cargo.toml)
if (existsSync(path.join(cwd, "Cargo.toml"))) {
if (await fileExists(path.join(cwd, "Cargo.toml"))) {
return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
}
// Check for TypeScript (tsconfig.json)
if (existsSync(path.join(cwd, "tsconfig.json"))) {
if (await fileExists(path.join(cwd, "tsconfig.json"))) {
return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
}
// Check for Go (go.mod)
if (existsSync(path.join(cwd, "go.mod"))) {
if (await fileExists(path.join(cwd, "go.mod"))) {
return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
}
// Check for Python (pyproject.toml or pyrightconfig.json)
if (existsSync(path.join(cwd, "pyproject.toml")) || existsSync(path.join(cwd, "pyrightconfig.json"))) {
if (
(await fileExists(path.join(cwd, "pyproject.toml"))) ||
(await fileExists(path.join(cwd, "pyrightconfig.json")))
) {
return { type: "python", command: ["pyright"], description: "Python (pyright)" };
}
@@ -389,7 +401,7 @@ async function runWorkspaceDiagnostics(
cwd: string,
config: LspConfig,
): Promise<{ output: string; projectType: ProjectType }> {
const projectType = detectProjectType(cwd);
const projectType = await detectProjectType(cwd);
// For Rust, use flycheck via rust-analyzer if available
if (projectType.type === "rust") {
+4 -4
View File
@@ -1,5 +1,5 @@
import { homedir, platform } from "node:os";
import { join } from "node:path";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { TOML } from "bun";
@@ -66,11 +66,11 @@ function getConfigPath(): string {
const home = homedir();
switch (platform()) {
case "win32":
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "lspmux", "config.toml");
return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "lspmux", "config.toml");
case "darwin":
return join(home, "Library", "Application Support", "lspmux", "config.toml");
return path.join(home, "Library", "Application Support", "lspmux", "config.toml");
default:
return join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "lspmux", "config.toml");
return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "lspmux", "config.toml");
}
}
+40 -18
View File
@@ -5,8 +5,9 @@
* createAgentSession() options. The SDK does the heavy lifting.
*/
import * as fs from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join, resolve } from "node:path";
import * as path from "node:path";
import { createInterface } from "node:readline/promises";
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
import { postmortem } from "@oh-my-pi/pi-utils";
@@ -14,6 +15,7 @@ import chalk from "chalk";
import { type Args, parseArgs, printHelp } from "./cli/args";
import { parseConfigArgs, printConfigHelp, runConfigCommand } from "./cli/config-cli";
import { processFileArguments } from "./cli/file-processor";
import { parseJupyterArgs, printJupyterHelp, runJupyterCommand } from "./cli/jupyter-cli";
import { listModels } from "./cli/list-models";
import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
import { selectSession } from "./cli/session-picker";
@@ -176,12 +178,16 @@ async function prepareInitialMessage(
/**
* Resolve a session argument to a local or global session match.
*/
function resolveSessionMatch(sessionArg: string, cwd: string, sessionDir?: string): SessionInfo | undefined {
const sessions = SessionManager.list(cwd, sessionDir);
async function resolveSessionMatch(
sessionArg: string,
cwd: string,
sessionDir?: string,
): Promise<SessionInfo | undefined> {
const sessions = await SessionManager.list(cwd, sessionDir);
let matches = sessions.filter((session) => session.id.startsWith(sessionArg));
if (matches.length === 0 && !sessionDir) {
const globalSessions = SessionManager.listAll();
const globalSessions = await SessionManager.listAll();
matches = globalSessions.filter((session) => session.id.startsWith(sessionArg));
}
@@ -202,14 +208,14 @@ async function promptForkSession(session: SessionInfo): Promise<boolean> {
}
}
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | undefined {
async function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): Promise<string | undefined> {
if (parsed.continue || parsed.resume) {
return undefined;
}
const lastVersion = settingsManager.getLastChangelogVersion();
const changelogPath = getChangelogPath();
const entries = parseChangelog(changelogPath);
const entries = await parseChangelog(changelogPath);
if (!lastVersion) {
if (entries.length > 0) {
@@ -236,12 +242,12 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
return await SessionManager.open(sessionArg, parsed.sessionDir);
}
const match = resolveSessionMatch(sessionArg, cwd, parsed.sessionDir);
const match = await resolveSessionMatch(sessionArg, cwd, parsed.sessionDir);
if (!match) {
throw new Error(`Session "${sessionArg}" not found.`);
}
const normalizedCwd = resolve(cwd);
const normalizedMatchCwd = resolve(match.cwd || cwd);
const normalizedCwd = path.resolve(cwd);
const normalizedMatchCwd = path.resolve(match.cwd || cwd);
if (normalizedCwd !== normalizedMatchCwd) {
const shouldFork = await promptForkSession(match);
if (!shouldFork) {
@@ -274,7 +280,7 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
}
const normalizePath = (value: string) => {
const resolved = resolve(value);
const resolved = path.resolve(value);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
};
@@ -284,16 +290,16 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
return;
}
const isDirectory = async (path: string) => {
const isDirectory = async (p: string) => {
try {
const stat = await Bun.file(path).stat();
return stat.isDirectory();
const s = await fs.stat(p);
return s.isDirectory();
} catch {
return false;
}
};
const candidates = [join(home, "tmp"), "/tmp", "/var/tmp"];
const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
for (const candidate of candidates) {
try {
if (!(await isDirectory(candidate))) {
@@ -477,7 +483,7 @@ export async function main(args: string[]) {
// Initialize theme early with defaults (CLI commands need symbols)
// Will be re-initialized with user preferences later
initTheme();
await initTheme();
// Handle plugin subcommand before regular parsing
const pluginCmd = parsePluginArgs(args);
@@ -523,6 +529,17 @@ export async function main(args: string[]) {
return;
}
// Handle jupyter subcommand
const jupyterCmd = parseJupyterArgs(args);
if (jupyterCmd) {
if (args.includes("--help") || args.includes("-h")) {
printJupyterHelp();
return;
}
await runJupyterCommand(jupyterCmd);
return;
}
// Handle stats subcommand
const statsCmd = parseStatsArgs(args);
if (statsCmd) {
@@ -619,7 +636,12 @@ export async function main(args: string[]) {
settingsManager.applyOverrides({ modelRoles: roleOverrides });
}
initTheme(settingsManager.getTheme(), isInteractive, settingsManager.getSymbolPreset());
await initTheme(
settingsManager.getTheme(),
isInteractive,
settingsManager.getSymbolPreset(),
settingsManager.getColorBlindMode(),
);
time("initTheme");
// Show deprecation warnings in interactive mode
@@ -640,7 +662,7 @@ export async function main(args: string[]) {
// Handle --resume: show session picker
if (parsed.resume) {
const sessions = SessionManager.list(cwd, parsed.sessionDir);
const sessions = await SessionManager.list(cwd, parsed.sessionDir);
time("SessionManager.list");
if (sessions.length === 0) {
writeStdout(chalk.dim("No sessions found"));
@@ -722,7 +744,7 @@ export async function main(args: string[]) {
await runRpcMode(session);
} else if (isInteractive) {
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
const changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);
const changelogMarkdown = await getChangelogForDisplay(parsed, settingsManager);
const scopedModelsForDisplay = sessionOptions.scopedModels ?? scopedModels;
if (scopedModelsForDisplay.length > 0) {
+60 -46
View File
@@ -2,10 +2,12 @@
* One-time migrations that run on startup.
*/
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { getAgentDbPath, getAgentDir, getBinDir } from "./config";
import { AgentStorage } from "./session/agent-storage";
import type { AuthCredential } from "./session/auth-storage";
@@ -17,16 +19,16 @@ import type { AuthCredential } from "./session/auth-storage";
*/
export async function migrateAuthToAgentDb(): Promise<string[]> {
const agentDir = getAgentDir();
const oauthPath = join(agentDir, "oauth.json");
const settingsPath = join(agentDir, "settings.json");
const oauthPath = path.join(agentDir, "oauth.json");
const settingsPath = path.join(agentDir, "settings.json");
const storage = await AgentStorage.open(getAgentDbPath(agentDir));
const migrated: Record<string, AuthCredential[]> = {};
const providers: string[] = [];
if (existsSync(oauthPath)) {
try {
const oauth = await Bun.file(oauthPath).json();
try {
const oauth = JSON.parse(readFileSync(oauthPath, "utf-8"));
for (const [provider, cred] of Object.entries(oauth)) {
if (storage.listAuthCredentials(provider).length > 0) {
continue;
@@ -34,16 +36,19 @@ export async function migrateAuthToAgentDb(): Promise<string[]> {
migrated[provider] = [{ type: "oauth", ...(cred as object) } as AuthCredential];
providers.push(provider);
}
renameSync(oauthPath, `${oauthPath}.migrated`);
await fs.promises.rename(oauthPath, `${oauthPath}.migrated`);
} catch (error) {
logger.warn("Failed to migrate oauth.json", { path: oauthPath, error: String(error) });
}
} catch (err) {
if (!isEnoent(err)) {
logger.warn("Failed to read oauth.json", { path: oauthPath, error: String(err) });
}
}
if (existsSync(settingsPath)) {
try {
const settings = await Bun.file(settingsPath).json();
try {
const content = readFileSync(settingsPath, "utf-8");
const settings = JSON.parse(content);
if (settings.apiKeys && typeof settings.apiKeys === "object") {
for (const [provider, key] of Object.entries(settings.apiKeys)) {
if (typeof key !== "string") continue;
@@ -53,11 +58,15 @@ export async function migrateAuthToAgentDb(): Promise<string[]> {
providers.push(provider);
}
delete settings.apiKeys;
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
}
} catch (error) {
logger.warn("Failed to migrate settings.json apiKeys", { path: settingsPath, error: String(error) });
}
} catch (err) {
if (!isEnoent(err)) {
logger.warn("Failed to read settings.json", { path: settingsPath, error: String(err) });
}
}
for (const [provider, credentials] of Object.entries(migrated)) {
@@ -76,15 +85,14 @@ export async function migrateAuthToAgentDb(): Promise<string[]> {
*
* See: https://github.com/badlogic/pi-mono/issues/320
*/
export function migrateSessionsFromAgentRoot(): void {
export async function migrateSessionsFromAgentRoot(): Promise<void> {
const agentDir = getAgentDir();
// Find all .jsonl files directly in agentDir (not in subdirectories)
let files: string[];
try {
files = readdirSync(agentDir)
.filter((f) => f.endsWith(".jsonl"))
.map((f) => join(agentDir, f));
const entries = await fs.promises.readdir(agentDir);
files = entries.filter((f) => f.endsWith(".jsonl")).map((f) => path.join(agentDir, f));
} catch (error) {
logger.warn("Failed to read agent directory for session migration", { path: agentDir, error: String(error) });
return;
@@ -95,7 +103,13 @@ export function migrateSessionsFromAgentRoot(): void {
for (const file of files) {
try {
// Read first line to get session header
const content = readFileSync(file, "utf8");
let content: string;
try {
content = await Bun.file(file).text();
} catch (err) {
if (isEnoent(err)) continue;
throw err;
}
const firstLine = content.split("\n")[0];
if (!firstLine?.trim()) continue;
@@ -106,18 +120,18 @@ export function migrateSessionsFromAgentRoot(): void {
// Compute the correct session directory (same encoding as session-manager.ts)
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
const correctDir = join(agentDir, "sessions", safePath);
const correctDir = path.join(agentDir, "sessions", safePath);
// Create directory if needed
mkdirSync(correctDir, { recursive: true });
await fs.promises.mkdir(correctDir, { recursive: true });
// Move the file
const fileName = file.split("/").pop() || file.split("\\").pop();
const newPath = join(correctDir, fileName!);
const newPath = path.join(correctDir, fileName!);
if (existsSync(newPath)) continue; // Skip if target exists
if (fs.existsSync(newPath)) continue; // Skip if target exists
renameSync(file, newPath);
await fs.promises.rename(file, newPath);
} catch (error) {
logger.warn("Failed to migrate session file", { path: file, error: String(error) });
}
@@ -127,38 +141,38 @@ export function migrateSessionsFromAgentRoot(): void {
/**
* Move fd/rg binaries from tools/ to bin/ if they exist.
*/
function migrateToolsToBin(): void {
async function migrateToolsToBin(): Promise<void> {
const agentDir = getAgentDir();
const toolsDir = join(agentDir, "tools");
const toolsDir = path.join(agentDir, "tools");
const binDir = getBinDir();
if (!existsSync(toolsDir)) return;
if (!fs.existsSync(toolsDir)) return;
const binaries = ["fd", "rg", "fd.exe", "rg.exe"];
let movedAny = false;
for (const bin of binaries) {
const oldPath = join(toolsDir, bin);
const newPath = join(binDir, bin);
const oldPath = path.join(toolsDir, bin);
const newPath = path.join(binDir, bin);
if (!fs.existsSync(oldPath)) continue;
if (existsSync(oldPath)) {
if (!existsSync(binDir)) {
mkdirSync(binDir, { recursive: true });
if (!fs.existsSync(binDir)) {
await fs.promises.mkdir(binDir, { recursive: true });
}
if (!fs.existsSync(newPath)) {
try {
await fs.promises.rename(oldPath, newPath);
movedAny = true;
} catch (error) {
logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
}
if (!existsSync(newPath)) {
try {
renameSync(oldPath, newPath);
movedAny = true;
} catch (error) {
logger.warn("Failed to migrate binary", { from: oldPath, to: newPath, error: String(error) });
}
} else {
// Target exists, just delete the old one
try {
rmSync(oldPath, { force: true });
} catch {
// Ignore
}
} else {
// Target exists, just delete the old one
try {
await fs.promises.rm(oldPath, { force: true });
} catch {
// Ignore
}
}
}
@@ -180,8 +194,8 @@ export async function runMigrations(_cwd: string): Promise<{
}> {
// Then: run data migrations
const migratedAuthProviders = await migrateAuthToAgentDb();
migrateSessionsFromAgentRoot();
migrateToolsToBin();
await migrateSessionsFromAgentRoot();
await migrateToolsToBin();
return { migratedAuthProviders, deprecationWarnings: [] };
}
@@ -1,8 +1,10 @@
import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
import * as fs from "node:fs";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { dirname, join } from "path";
import { theme } from "../../modes/theme/theme";
import type { AgentSession } from "../../session/agent-session";
import { shortenPath } from "../../tools/render-utils";
@@ -18,20 +20,19 @@ function sanitizeStatusText(text: string): string {
.trim();
}
/**
* Find the git root directory by walking up from cwd.
* Returns the path to .git/HEAD if found, null otherwise.
*/
function findGitHeadPath(): string | null {
/** Find the git root by walking up from cwd. Returns path and content of .git/HEAD if found. */
async function findGitHeadPath(): Promise<{ path: string; content: string } | null> {
let dir = process.cwd();
while (true) {
const gitHeadPath = join(dir, ".git", "HEAD");
if (existsSync(gitHeadPath)) {
return gitHeadPath;
try {
const content = await Bun.file(gitHeadPath).text();
return { path: gitHeadPath, content };
} catch (err) {
if (!isEnoent(err)) throw err;
}
const parent = dirname(dir);
if (parent === dir) {
// Reached filesystem root
return null;
}
dir = parent;
@@ -44,7 +45,7 @@ function findGitHeadPath(): string | null {
export class FooterComponent implements Component {
private session: AgentSession;
private cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
private gitWatcher: FSWatcher | null = null;
private gitWatcher: fs.FSWatcher | null = null;
private onBranchChange: (() => void) | null = null;
private autoCompactEnabled: boolean = true;
private extensionStatuses: Map<string, string> = new Map();
@@ -88,21 +89,22 @@ export class FooterComponent implements Component {
this.gitWatcher = null;
}
const gitHeadPath = findGitHeadPath();
if (!gitHeadPath) {
return;
}
findGitHeadPath().then((result) => {
if (!result) {
return;
}
try {
this.gitWatcher = watch(gitHeadPath, () => {
this.cachedBranch = undefined; // Invalidate cache
if (this.onBranchChange) {
this.onBranchChange();
}
});
} catch {
// Silently fail if we can't watch
}
try {
this.gitWatcher = fs.watch(result.path, () => {
this.cachedBranch = undefined; // Invalidate cache
if (this.onBranchChange) {
this.onBranchChange();
}
});
} catch {
// Silently fail if we can't watch
}
});
}
/**
@@ -130,27 +132,24 @@ export class FooterComponent implements Component {
return this.cachedBranch;
}
try {
const gitHeadPath = findGitHeadPath();
if (!gitHeadPath) {
// Note: fire-and-forget async call - will return undefined on first call
// This is acceptable since it's a cached value that will update on next render
findGitHeadPath().then((result) => {
if (!result) {
this.cachedBranch = null;
return null;
return;
}
const content = readFileSync(gitHeadPath, "utf8").trim();
const content = result.content.trim();
if (content.startsWith("ref: refs/heads/")) {
// Normal branch: extract branch name
this.cachedBranch = content.slice(16);
} else {
// Detached HEAD state
this.cachedBranch = "detached";
}
} catch {
// Not in a git repo or error reading file
this.cachedBranch = null;
}
});
return this.cachedBranch;
// Return undefined while loading (will show on next render once loaded)
return null;
}
render(width: number): string[] {
@@ -3,7 +3,7 @@
* Supports Ctrl+G for external editor.
*/
import { rm } from "node:fs/promises";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
@@ -110,7 +110,7 @@ export class HookEditorComponent extends Container {
}
} finally {
try {
await rm(tmpFile, { force: true });
await fs.rm(tmpFile, { force: true });
} catch {
// Ignore cleanup errors
}
@@ -132,10 +132,10 @@ export class PluginDetailComponent extends Container {
this.manager = manager;
this.callbacks = callbacks;
this.rebuild();
void this.rebuild();
}
private rebuild(): void {
private async rebuild(): Promise<void> {
this.clear();
const plugin = this.plugin;
@@ -184,7 +184,7 @@ export class PluginDetailComponent extends Container {
// Config settings
if (manifest.settings && Object.keys(manifest.settings).length > 0) {
const settings = this.manager.getPluginSettings(plugin.name);
const settings = await this.manager.getPluginSettings(plugin.name);
for (const [key, schema] of Object.entries(manifest.settings)) {
const currentValue = settings[key] ?? schema.default;
@@ -456,7 +456,7 @@ export class PluginSettingsComponent extends Container {
this.callbacks.onPluginChanged();
},
onFeatureChange: async (feature, enabled) => {
const current = new Set(this.manager.getEnabledFeatures(plugin.name) ?? []);
const current = new Set((await this.manager.getEnabledFeatures(plugin.name)) ?? []);
if (enabled) {
current.add(feature);
} else {
@@ -465,8 +465,8 @@ export class PluginSettingsComponent extends Container {
await this.manager.setEnabledFeatures(plugin.name, [...current]);
this.callbacks.onPluginChanged();
},
onConfigChange: (key, value) => {
this.manager.setPluginSetting(plugin.name, key, value);
onConfigChange: async (key, value) => {
await this.manager.setPluginSetting(plugin.name, key, value);
this.callbacks.onPluginChanged();
},
onBack: () => this.showPluginList(),
@@ -422,6 +422,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
{ value: "ascii", label: "ASCII", description: "ASCII-only characters (maximum compatibility)" },
],
},
{
id: "colorBlindMode",
tab: "display",
type: "boolean",
label: "Color blind mode",
description: "Use blue instead of green for diff additions (red-green color blindness)",
get: (sm) => sm.getColorBlindMode(),
set: (sm, v) => sm.setColorBlindMode(v),
},
{
id: "thinkingLevel",
tab: "display",
@@ -1,9 +1,10 @@
import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
import { getAvailableThemes, getSelectListTheme } from "../../modes/theme/theme";
import { getSelectListTheme } from "../../modes/theme/theme";
import { DynamicBorder } from "./dynamic-border";
/**
* Component that renders a theme selector
* Component that renders a theme selector.
* Themes must be pre-loaded and passed to the constructor.
*/
export class ThemeSelectorComponent extends Container {
private selectList: SelectList;
@@ -11,6 +12,7 @@ export class ThemeSelectorComponent extends Container {
constructor(
currentTheme: string,
themes: string[],
onSelect: (themeName: string) => void,
onCancel: () => void,
onPreview: (themeName: string) => void,
@@ -18,8 +20,7 @@ export class ThemeSelectorComponent extends Container {
super();
this.onPreview = onPreview;
// Get available themes and create select items
const themes = getAvailableThemes();
// Create select items from provided themes
const themeItems: SelectItem[] = themes.map((name) => ({
value: name,
label: name,
@@ -1,4 +1,4 @@
import { mkdir, rm } from "node:fs/promises";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
@@ -77,7 +77,7 @@ export class CommandController {
const tmpFile = path.join(os.tmpdir(), `${nanoid()}.html`);
const cleanupTempFile = async () => {
try {
await rm(tmpFile, { force: true });
await fs.rm(tmpFile, { force: true });
} catch {
// Ignore cleanup errors
}
@@ -212,7 +212,7 @@ export class CommandController {
}
}
handleSessionCommand(): void {
async handleSessionCommand(): Promise<void> {
const stats = this.ctx.session.getSessionStats();
let info = `${theme.bold("Session Info")}\n\n`;
@@ -240,7 +240,7 @@ export class CommandController {
info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}\n`;
}
const gateway = getGatewayStatus();
const gateway = await getGatewayStatus();
info += `\n${theme.bold("Python Gateway")}\n`;
if (gateway.active) {
info += `${theme.fg("dim", "Status:")} ${theme.fg("success", "Active (Global)")}\n`;
@@ -316,9 +316,9 @@ export class CommandController {
this.ctx.ui.requestRender();
}
handleChangelogCommand(): void {
async handleChangelogCommand(): Promise<void> {
const changelogPath = getChangelogPath();
const allEntries = parseChangelog(changelogPath);
const allEntries = await parseChangelog(changelogPath);
const changelogMarkdown =
allEntries.length > 0
@@ -439,7 +439,7 @@ export class CommandController {
].join("\n");
try {
await mkdir(path.dirname(debugLogPath), { recursive: true });
await fs.mkdir(path.dirname(debugLogPath), { recursive: true });
await Bun.write(debugLogPath, debugData);
} catch (error) {
this.ctx.showError(`Failed to write debug log: ${error instanceof Error ? error.message : String(error)}`);
@@ -39,14 +39,14 @@ export class ExtensionUiController {
get theme() {
return theme;
},
getAllThemes: () => getAvailableThemesWithPaths().map((t) => ({ name: t.name, path: t.path })),
getAllThemes: async () => (await getAvailableThemesWithPaths()).map((t) => ({ name: t.name, path: t.path })),
getTheme: (name) => getThemeByName(name),
setTheme: (themeArg) => {
setTheme: async (themeArg) => {
if (typeof themeArg === "string") {
return setTheme(themeArg, true);
return await setTheme(themeArg, true);
}
// Theme object passed directly - not supported in current implementation
return { success: false, error: "Direct theme object not supported" };
return Promise.resolve({ success: false, error: "Direct theme object not supported" });
},
setFooter: () => {},
setHeader: () => {},
@@ -398,9 +398,9 @@ export class ExtensionUiController {
get theme() {
return theme;
},
getAllThemes: () => [],
getTheme: () => undefined,
setTheme: () => ({ success: false, error: "Background mode" }),
getAllThemes: () => Promise.resolve([]),
getTheme: () => Promise.resolve(undefined),
setTheme: () => Promise.resolve({ success: false, error: "Background mode" }),
setFooter: () => {},
setHeader: () => {},
setEditorComponent: () => {},
@@ -1,6 +1,5 @@
import { spawn } from "node:child_process";
import type { FileHandle } from "node:fs/promises";
import { open, rm } from "node:fs/promises";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
@@ -207,7 +206,7 @@ export class InputController {
return;
}
if (text === "/session") {
this.ctx.handleSessionCommand();
await this.ctx.handleSessionCommand();
this.ctx.editor.setText("");
return;
}
@@ -217,7 +216,7 @@ export class InputController {
return;
}
if (text === "/changelog") {
this.ctx.handleChangelogCommand();
await this.ctx.handleChangelogCommand();
this.ctx.editor.setText("");
return;
}
@@ -638,13 +637,13 @@ export class InputController {
return "/dev/tty";
}
private async openEditorTerminalHandle(): Promise<FileHandle | null> {
private async openEditorTerminalHandle(): Promise<fs.FileHandle | null> {
const terminalPath = this.getEditorTerminalPath();
if (!terminalPath) {
return null;
}
try {
return await open(terminalPath, "r+");
return await fs.open(terminalPath, "r+");
} catch {
return null;
}
@@ -661,7 +660,7 @@ export class InputController {
const currentText = this.ctx.editor.getText();
const tmpFile = path.join(os.tmpdir(), `omp-editor-${nanoid()}.omp.md`);
let ttyHandle: FileHandle | null = null;
let ttyHandle: fs.FileHandle | null = null;
try {
// Write current content to temp file
await Bun.write(tmpFile, currentText);
@@ -696,7 +695,7 @@ export class InputController {
} finally {
// Clean up temp file
try {
await rm(tmpFile, { force: true });
await fs.rm(tmpFile, { force: true });
} catch {
// Ignore cleanup errors
}
@@ -14,7 +14,14 @@ import { SettingsSelectorComponent } from "../../modes/components/settings-selec
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
import { TreeSelectorComponent } from "../../modes/components/tree-selector";
import { UserMessageSelectorComponent } from "../../modes/components/user-message-selector";
import { getAvailableThemes, getSymbolTheme, setSymbolPreset, setTheme, theme } from "../../modes/theme/theme";
import {
getAvailableThemes,
getSymbolTheme,
setColorBlindMode,
setSymbolPreset,
setTheme,
theme,
} from "../../modes/theme/theme";
import type { InteractiveModeContext } from "../../modes/types";
import { SessionManager } from "../../session/session-manager";
import { setPreferredImageProvider, setPreferredWebSearchProvider } from "../../tools";
@@ -40,49 +47,52 @@ export class SelectorController {
}
showSettingsSelector(): void {
this.showSelector((done) => {
const selector = new SettingsSelectorComponent(
this.ctx.settingsManager,
{
availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(),
thinkingLevel: this.ctx.session.thinkingLevel,
availableThemes: getAvailableThemes(),
cwd: process.cwd(),
},
{
onChange: (id, value) => this.handleSettingChange(id, value),
onThemePreview: (themeName) => {
const result = setTheme(themeName, true);
if (result.success) {
this.ctx.ui.invalidate();
getAvailableThemes().then((availableThemes) => {
this.showSelector((done) => {
const selector = new SettingsSelectorComponent(
this.ctx.settingsManager,
{
availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(),
thinkingLevel: this.ctx.session.thinkingLevel,
availableThemes,
cwd: process.cwd(),
},
{
onChange: (id, value) => this.handleSettingChange(id, value),
onThemePreview: (themeName) => {
setTheme(themeName, true).then((result) => {
if (result.success) {
this.ctx.ui.invalidate();
this.ctx.ui.requestRender();
}
});
},
onStatusLinePreview: (settings) => {
// Update status line with preview settings
const currentSettings = this.ctx.settingsManager.getStatusLineSettings();
this.ctx.statusLine.updateSettings({ ...currentSettings, ...settings });
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
}
},
getStatusLinePreview: () => {
// Return the rendered status line for inline preview
const width = this.ctx.ui.getWidth();
return this.ctx.statusLine.getTopBorder(width).content;
},
onPluginsChanged: () => {
this.ctx.ui.requestRender();
},
onCancel: () => {
done();
// Restore status line to saved settings
this.ctx.statusLine.updateSettings(this.ctx.settingsManager.getStatusLineSettings());
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
},
},
onStatusLinePreview: (settings) => {
// Update status line with preview settings
const currentSettings = this.ctx.settingsManager.getStatusLineSettings();
this.ctx.statusLine.updateSettings({ ...currentSettings, ...settings });
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
},
getStatusLinePreview: () => {
// Return the rendered status line for inline preview
const width = this.ctx.ui.getWidth();
return this.ctx.statusLine.getTopBorder(width).content;
},
onPluginsChanged: () => {
this.ctx.ui.requestRender();
},
onCancel: () => {
done();
// Restore status line to saved settings
this.ctx.statusLine.updateSettings(this.ctx.settingsManager.getStatusLineSettings());
this.ctx.updateEditorTopBorder();
this.ctx.ui.requestRender();
},
},
);
return { component: selector, focus: selector };
);
return { component: selector, focus: selector };
});
});
}
@@ -183,20 +193,28 @@ export class SelectorController {
this.ctx.rebuildChatFromMessages();
break;
case "theme": {
const result = setTheme(value as string, true);
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
if (!result.success) {
this.ctx.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
}
setTheme(value as string, true).then((result) => {
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
if (!result.success) {
this.ctx.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
}
});
break;
}
case "symbolPreset": {
setSymbolPreset(value as "unicode" | "nerd" | "ascii");
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
setSymbolPreset(value as "unicode" | "nerd" | "ascii").then(() => {
this.ctx.statusLine.invalidate();
this.ctx.updateEditorTopBorder();
this.ctx.ui.invalidate();
});
break;
}
case "colorBlindMode": {
setColorBlindMode(value === "true" || value === true).then(() => {
this.ctx.ui.invalidate();
});
break;
}
case "statusLinePreset":
@@ -447,12 +465,12 @@ export class SelectorController {
});
}
showSessionSelector(): void {
async showSessionSelector(): Promise<void> {
const sessions = await SessionManager.list(
this.ctx.sessionManager.getCwd(),
this.ctx.sessionManager.getSessionDir(),
);
this.showSelector((done) => {
const sessions = SessionManager.list(
this.ctx.sessionManager.getCwd(),
this.ctx.sessionManager.getSessionDir(),
);
const selector = new SessionSelectorComponent(
sessions,
async (sessionPath) => {
@@ -16,7 +16,7 @@ import {
Text,
TUI,
} from "@oh-my-pi/pi-tui";
import { logger, postmortem } from "@oh-my-pi/pi-utils";
import { isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { KeybindingsManager } from "../config/keybindings";
import type { SettingsManager } from "../config/settings-manager";
@@ -268,7 +268,7 @@ export class InteractiveMode implements InteractiveModeContext {
const providerName = this.session.model?.provider ?? "Unknown";
// Get recent sessions
const recentSessions = getRecentSessions(this.sessionManager.getSessionDir()).map((s) => ({
const recentSessions = (await getRecentSessions(this.sessionManager.getSessionDir())).map((s) => ({
name: s.name,
timeAgo: s.timeAgo,
}));
@@ -452,17 +452,16 @@ export class InteractiveMode implements InteractiveModeContext {
}
const artifactsDir = sessionFile.slice(0, -6);
const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
const file = Bun.file(todoPath);
if (!(await file.exists())) {
this.renderTodoList();
return;
}
try {
const data = (await file.json()) as { todos?: TodoItem[] };
const data = (await Bun.file(todoPath).json()) as { todos?: TodoItem[] };
if (data?.todos && Array.isArray(data.todos)) {
this.todoItems = data.todos;
}
} catch (error) {
if (isEnoent(error)) {
this.renderTodoList();
return;
}
logger.warn("Failed to load todos", { path: todoPath, error: String(error) });
}
this.renderTodoList();
@@ -612,16 +611,16 @@ export class InteractiveMode implements InteractiveModeContext {
return this.commandController.handleCopyCommand();
}
handleSessionCommand(): void {
this.commandController.handleSessionCommand();
handleSessionCommand(): Promise<void> {
return this.commandController.handleSessionCommand();
}
handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
return this.commandController.handleUsageCommand(reports);
}
handleChangelogCommand(): void {
this.commandController.handleChangelogCommand();
async handleChangelogCommand(): Promise<void> {
await this.commandController.handleChangelogCommand();
}
handleHotkeysCommand(): void {
@@ -271,17 +271,17 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
return theme;
}
getAllThemes(): { name: string; path: string | undefined }[] {
return [];
getAllThemes(): Promise<{ name: string; path: string | undefined }[]> {
return Promise.resolve([]);
}
getTheme(_name: string): Theme | undefined {
return undefined;
getTheme(_name: string): Promise<Theme | undefined> {
return Promise.resolve(undefined);
}
setTheme(_theme: string | Theme): { success: boolean; error?: string } {
setTheme(_theme: string | Theme): Promise<{ success: boolean; error?: string }> {
// Theme switching not supported in RPC mode
return { success: false, error: "Theme switching not supported in RPC mode" };
return Promise.resolve({ success: false, error: "Theme switching not supported in RPC mode" });
}
setEditorComponent(): void {
+123 -48
View File
@@ -1,7 +1,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
import { logger } from "@oh-my-pi/pi-utils";
import { adjustHsv, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { type Static, Type } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import chalk from "chalk";
@@ -1600,16 +1600,18 @@ function getBuiltinThemes(): Record<string, ThemeJson> {
return BUILTIN_THEMES;
}
export function getAvailableThemes(): string[] {
export async function getAvailableThemes(): Promise<string[]> {
const themes = new Set<string>(Object.keys(getBuiltinThemes()));
const customThemesDir = getCustomThemesDir();
if (fs.existsSync(customThemesDir)) {
const files = fs.readdirSync(customThemesDir);
try {
const files = await fs.promises.readdir(customThemesDir);
for (const file of files) {
if (file.endsWith(".json")) {
themes.add(file.slice(0, -5));
}
}
} catch {
// Directory doesn't exist or isn't readable
}
return Array.from(themes).sort();
}
@@ -1619,7 +1621,7 @@ export interface ThemeInfo {
path: string | undefined;
}
export function getAvailableThemesWithPaths(): ThemeInfo[] {
export async function getAvailableThemesWithPaths(): Promise<ThemeInfo[]> {
const result: ThemeInfo[] = [];
// Built-in themes (embedded, no file path)
@@ -1629,8 +1631,9 @@ export function getAvailableThemesWithPaths(): ThemeInfo[] {
// Custom themes
const customThemesDir = getCustomThemesDir();
if (fs.existsSync(customThemesDir)) {
for (const file of fs.readdirSync(customThemesDir)) {
try {
const files = await fs.promises.readdir(customThemesDir);
for (const file of files) {
if (file.endsWith(".json")) {
const name = file.slice(0, -5);
if (!result.some((themeInfo) => themeInfo.name === name)) {
@@ -1638,22 +1641,27 @@ export function getAvailableThemesWithPaths(): ThemeInfo[] {
}
}
}
} catch {
// Directory doesn't exist or isn't readable
}
return result.sort((a, b) => a.name.localeCompare(b.name));
}
function loadThemeJson(name: string): ThemeJson {
async function loadThemeJson(name: string): Promise<ThemeJson> {
const builtinThemes = getBuiltinThemes();
if (name in builtinThemes) {
return builtinThemes[name];
}
const customThemesDir = getCustomThemesDir();
const themePath = path.join(customThemesDir, `${name}.json`);
if (!fs.existsSync(themePath)) {
throw new Error(`Theme not found: ${name}`);
let content: string;
try {
content = await Bun.file(themePath).text();
} catch (err) {
if (isEnoent(err)) throw new Error(`Theme not found: ${name}`);
throw err;
}
const content = fs.readFileSync(themePath, "utf-8");
let json: unknown;
try {
json = JSON.parse(content);
@@ -1691,9 +1699,27 @@ function loadThemeJson(name: string): ThemeJson {
return json as ThemeJson;
}
function createTheme(themeJson: ThemeJson, mode?: ColorMode, symbolPresetOverride?: SymbolPreset): Theme {
interface CreateThemeOptions {
mode?: ColorMode;
symbolPresetOverride?: SymbolPreset;
colorBlindMode?: boolean;
}
/** HSV adjustment to shift green toward blue for colorblind mode (red-green colorblindness) */
const COLORBLIND_ADJUSTMENT = { h: 60, s: 0.71 };
function createTheme(themeJson: ThemeJson, options: CreateThemeOptions = {}): Theme {
const { mode, symbolPresetOverride, colorBlindMode } = options;
const colorMode = mode ?? detectColorMode();
const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars);
if (colorBlindMode) {
const added = resolvedColors.toolDiffAdded;
if (typeof added === "string" && added.startsWith("#")) {
resolvedColors.toolDiffAdded = adjustHsv(added, COLORBLIND_ADJUSTMENT);
}
}
const fgColors: Record<ThemeColor, string | number> = {} as Record<ThemeColor, string | number>;
const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
const bgColorKeys: Set<string> = new Set([
@@ -1718,14 +1744,14 @@ function createTheme(themeJson: ThemeJson, mode?: ColorMode, symbolPresetOverrid
return new Theme(fgColors, bgColors, colorMode, symbolPreset, symbolOverrides);
}
function loadTheme(name: string, mode?: ColorMode, symbolPresetOverride?: SymbolPreset): Theme {
const themeJson = loadThemeJson(name);
return createTheme(themeJson, mode, symbolPresetOverride);
async function loadTheme(name: string, options: CreateThemeOptions = {}): Promise<Theme> {
const themeJson = await loadThemeJson(name);
return createTheme(themeJson, options);
}
export function getThemeByName(name: string): Theme | undefined {
export async function getThemeByName(name: string): Promise<Theme | undefined> {
try {
return loadTheme(name);
return await loadTheme(name);
} catch {
return undefined;
}
@@ -1757,32 +1783,49 @@ function getDefaultTheme(): string {
export let theme: Theme;
let currentThemeName: string | undefined;
let currentSymbolPresetOverride: SymbolPreset | undefined;
let currentColorBlindMode: boolean = false;
let themeWatcher: fs.FSWatcher | undefined;
let onThemeChangeCallback: (() => void) | undefined;
export function initTheme(themeName?: string, enableWatcher: boolean = false, symbolPreset?: SymbolPreset): void {
function getCurrentThemeOptions(): CreateThemeOptions {
return {
symbolPresetOverride: currentSymbolPresetOverride,
colorBlindMode: currentColorBlindMode,
};
}
export async function initTheme(
themeName?: string,
enableWatcher: boolean = false,
symbolPreset?: SymbolPreset,
colorBlindMode?: boolean,
): Promise<void> {
const name = themeName ?? getDefaultTheme();
currentThemeName = name;
currentSymbolPresetOverride = symbolPreset;
currentColorBlindMode = colorBlindMode ?? false;
try {
theme = loadTheme(name, undefined, symbolPreset);
theme = await loadTheme(name, getCurrentThemeOptions());
if (enableWatcher) {
startThemeWatcher();
await startThemeWatcher();
}
} catch (err) {
logger.debug("Theme loading failed, falling back to dark theme", { error: String(err) });
currentThemeName = "dark";
theme = loadTheme("dark", undefined, symbolPreset);
theme = await loadTheme("dark", getCurrentThemeOptions());
// Don't start watcher for fallback theme
}
}
export function setTheme(name: string, enableWatcher: boolean = false): { success: boolean; error?: string } {
export async function setTheme(
name: string,
enableWatcher: boolean = false,
): Promise<{ success: boolean; error?: string }> {
currentThemeName = name;
try {
theme = loadTheme(name, undefined, currentSymbolPresetOverride);
theme = await loadTheme(name, getCurrentThemeOptions());
if (enableWatcher) {
startThemeWatcher();
await startThemeWatcher();
}
if (onThemeChangeCallback) {
onThemeChangeCallback();
@@ -1791,7 +1834,7 @@ export function setTheme(name: string, enableWatcher: boolean = false): { succes
} catch (error) {
// Theme is invalid - fall back to dark theme
currentThemeName = "dark";
theme = loadTheme("dark", undefined, currentSymbolPresetOverride);
theme = await loadTheme("dark", getCurrentThemeOptions());
// Don't start watcher for fallback theme
return {
success: false,
@@ -1812,14 +1855,14 @@ export function setThemeInstance(themeInstance: Theme): void {
/**
* Set the symbol preset override, recreating the theme with the new preset.
*/
export function setSymbolPreset(preset: SymbolPreset): void {
export async function setSymbolPreset(preset: SymbolPreset): Promise<void> {
currentSymbolPresetOverride = preset;
if (currentThemeName) {
try {
theme = loadTheme(currentThemeName, undefined, preset);
theme = await loadTheme(currentThemeName, getCurrentThemeOptions());
} catch {
// Fall back to dark theme with new preset
theme = loadTheme("dark", undefined, preset);
theme = await loadTheme("dark", getCurrentThemeOptions());
}
if (onThemeChangeCallback) {
onThemeChangeCallback();
@@ -1834,6 +1877,32 @@ export function getSymbolPresetOverride(): SymbolPreset | undefined {
return currentSymbolPresetOverride;
}
/**
* Set color blind mode, recreating the theme with the new setting.
* When enabled, uses blue instead of green for diff additions.
*/
export async function setColorBlindMode(enabled: boolean): Promise<void> {
currentColorBlindMode = enabled;
if (currentThemeName) {
try {
theme = await loadTheme(currentThemeName, getCurrentThemeOptions());
} catch {
// Fall back to dark theme
theme = await loadTheme("dark", getCurrentThemeOptions());
}
if (onThemeChangeCallback) {
onThemeChangeCallback();
}
}
}
/**
* Get the current color blind mode setting.
*/
export function getColorBlindMode(): boolean {
return currentColorBlindMode;
}
export function onThemeChange(callback: () => void): void {
onThemeChangeCallback = callback;
}
@@ -1852,7 +1921,7 @@ export function isValidSymbolPreset(preset: string): preset is SymbolPreset {
return preset === "unicode" || preset === "nerd" || preset === "ascii";
}
function startThemeWatcher(): void {
async function startThemeWatcher(): Promise<void> {
// Stop existing watcher if any
if (themeWatcher) {
themeWatcher.close();
@@ -1877,30 +1946,36 @@ function startThemeWatcher(): void {
if (eventType === "change") {
// Debounce rapid changes
setTimeout(() => {
try {
// Reload the theme with current symbol preset override
theme = loadTheme(currentThemeName!, undefined, currentSymbolPresetOverride);
// Notify callback (to invalidate UI)
if (onThemeChangeCallback) {
onThemeChangeCallback();
}
} catch (err) {
logger.debug("Theme reload error during file change", { error: String(err) });
}
loadTheme(currentThemeName!, getCurrentThemeOptions())
.then((loadedTheme) => {
theme = loadedTheme;
if (onThemeChangeCallback) {
onThemeChangeCallback();
}
})
.catch((err) => {
logger.debug("Theme reload error during file change", { error: String(err) });
});
}, 100);
} else if (eventType === "rename") {
// File was deleted or renamed - fall back to default theme
setTimeout(() => {
if (!fs.existsSync(themeFile)) {
currentThemeName = "dark";
theme = loadTheme("dark");
loadTheme("dark", getCurrentThemeOptions())
.then((loadedTheme) => {
theme = loadedTheme;
if (onThemeChangeCallback) {
onThemeChangeCallback();
}
})
.catch((err) => {
logger.debug("Theme reload error during rename fallback", { error: String(err) });
});
if (themeWatcher) {
themeWatcher.close();
themeWatcher = undefined;
}
if (onThemeChangeCallback) {
onThemeChangeCallback();
}
}
}, 100);
}
@@ -1971,10 +2046,10 @@ function ansi256ToHex(index: number): string {
* Get resolved theme colors as CSS-compatible hex strings.
* Used by HTML export to generate CSS custom properties.
*/
export function getResolvedThemeColors(themeName?: string): Record<string, string> {
export async function getResolvedThemeColors(themeName?: string): Promise<Record<string, string>> {
const name = themeName ?? getDefaultTheme();
const isLight = name === "light";
const themeJson = loadThemeJson(name);
const themeJson = await loadThemeJson(name);
const resolved = resolveThemeColors(themeJson.colors, themeJson.vars);
// Default text color for empty values (terminal uses default fg color)
@@ -2006,14 +2081,14 @@ export function isLightTheme(themeName?: string): boolean {
* Get explicit export colors from theme JSON, if specified.
* Returns undefined for each color that isn't explicitly set.
*/
export function getThemeExportColors(themeName?: string): {
export async function getThemeExportColors(themeName?: string): Promise<{
pageBg?: string;
cardBg?: string;
infoBg?: string;
} {
}> {
const name = themeName ?? getDefaultTheme();
try {
const themeJson = loadThemeJson(name);
const themeJson = await loadThemeJson(name);
const exportSection = themeJson.export;
if (!exportSection) return {};
+2 -2
View File
@@ -132,9 +132,9 @@ export interface InteractiveModeContext {
handleExportCommand(text: string): Promise<void>;
handleShareCommand(): Promise<void>;
handleCopyCommand(): Promise<void>;
handleSessionCommand(): void;
handleSessionCommand(): Promise<void>;
handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
handleChangelogCommand(): void;
handleChangelogCommand(): Promise<void>;
handleHotkeysCommand(): void;
handleDumpCommand(): Promise<void>;
handleClearCommand(): Promise<void>;
@@ -5,8 +5,8 @@
* for robust handling of whitespace and formatting differences.
*/
import { mkdirSync, unlinkSync } from "node:fs";
import { dirname } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { resolveToCwd } from "../tools/path-utils";
import { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch, seekSequence } from "./fuzzy";
import {
@@ -51,10 +51,10 @@ export const defaultFileSystem: FileSystem = {
await Bun.write(path, content);
},
async delete(path: string): Promise<void> {
unlinkSync(path);
await fs.unlink(path);
},
async mkdir(path: string): Promise<void> {
mkdirSync(path, { recursive: true });
await fs.mkdir(path, { recursive: true });
},
};
@@ -1113,7 +1113,7 @@ async function applyNormalizedPatch(
const content = normalizedContent.endsWith("\n") ? normalizedContent : `${normalizedContent}\n`;
if (!dryRun) {
const parentDir = dirname(absolutePath);
const parentDir = path.dirname(absolutePath);
if (parentDir && parentDir !== ".") {
await fs.mkdir(parentDir);
}
@@ -1182,7 +1182,7 @@ async function applyNormalizedPatch(
if (!dryRun) {
if (isMove) {
const parentDir = dirname(destPath);
const parentDir = path.dirname(destPath);
if (parentDir && parentDir !== ".") {
await fs.mkdir(parentDir);
}
+2 -2
View File
@@ -8,7 +8,7 @@
* The mode is determined by the `edit.patchMode` setting.
*/
import { mkdir } from "node:fs/promises";
import * as fs from "node:fs/promises";
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import { StringEnum } from "@oh-my-pi/pi-ai";
import { Type } from "@sinclair/typebox";
@@ -160,7 +160,7 @@ class LspFileSystem implements FileSystem {
}
async mkdir(path: string): Promise<void> {
await mkdir(path, { recursive: true });
await fs.mkdir(path, { recursive: true });
}
getDiagnostics(): FileDiagnosticsResult | undefined {
+26 -20
View File
@@ -26,9 +26,8 @@
* ```
*/
import { existsSync } from "node:fs";
import { rename } from "node:fs/promises";
import { join } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
import type { Component } from "@oh-my-pi/pi-tui";
@@ -44,12 +43,9 @@ import { formatModelString, parseModelString } from "./config/model-resolver";
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
import { type Settings, SettingsManager, type SkillsSettings } from "./config/settings-manager";
import { CursorExecHandlers } from "./cursor";
import "./discovery";
import { initializeWithSettings } from "./discovery";
import { TtsrManager } from "./export/ttsr";
import { disposeAllKernelSessions } from "./ipy/executor";
import { closeAllConnections } from "./ssh/connection-manager";
import { unmountAll } from "./ssh/sshfs-mount";
import "./discovery";
import {
type CustomCommandsLoadResult,
loadCustomCommands as loadCustomCommandsInternal,
@@ -77,12 +73,15 @@ import {
RuleProtocolHandler,
SkillProtocolHandler,
} from "./internal-urls";
import { disposeAllKernelSessions } from "./ipy/executor";
import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
import { AgentSession } from "./session/agent-session";
import { AuthStorage } from "./session/auth-storage";
import { convertToLlm } from "./session/messages";
import { SessionManager } from "./session/session-manager";
import { migrateJsonStorage } from "./session/storage-migration";
import { closeAllConnections } from "./ssh/connection-manager";
import { unmountAll } from "./ssh/sshfs-mount";
import {
buildSystemPrompt as buildSystemPromptInternal,
loadProjectContextFiles as loadContextFilesInternal,
@@ -207,10 +206,7 @@ export interface CreateAgentSessionResult {
export type { PromptTemplate } from "./config/prompt-templates";
export type { Settings, SkillsSettings } from "./config/settings-manager";
export type {
CustomCommand,
CustomCommandFactory,
} from "./extensibility/custom-commands/types";
export type { CustomCommand, CustomCommandFactory } from "./extensibility/custom-commands/types";
export type { CustomTool, CustomToolFactory } from "./extensibility/custom-tools/types";
export type {
ExtensionAPI,
@@ -254,7 +250,7 @@ function getDefaultAgentDir(): string {
* Reads from primary path first, then falls back to legacy paths (.pi, .claude).
*/
export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise<AuthStorage> {
const primaryPath = join(agentDir, "auth.json");
const primaryPath = path.join(agentDir, "auth.json");
// Get all auth.json paths (user-level only), excluding the primary
const allPaths = getConfigDirPaths("auth.json", { project: false });
const fallbackPaths = allPaths.filter((p) => p !== primaryPath);
@@ -264,7 +260,7 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
// Migrate legacy JSON files (settings.json, auth.json) to SQLite before loading
await migrateJsonStorage({
agentDir,
settingsPath: join(agentDir, "settings.json"),
settingsPath: path.join(agentDir, "settings.json"),
authPaths: [primaryPath, ...fallbackPaths],
});
@@ -282,27 +278,37 @@ export async function discoverModels(
authStorage: AuthStorage,
agentDir: string = getDefaultAgentDir(),
): Promise<ModelRegistry> {
const yamlPath = join(agentDir, "models.yml");
const jsonPath = join(agentDir, "models.json");
const yamlPath = path.join(agentDir, "models.yml");
const jsonPath = path.join(agentDir, "models.json");
// Check existence of yaml and json files
const [yamlExists, jsonExists] = await Promise.all([Bun.file(yamlPath).exists(), Bun.file(jsonPath).exists()]);
// Migrate models.json to models.yml if yaml doesn't exist but json does
if (!existsSync(yamlPath) && existsSync(jsonPath)) {
if (!yamlExists && jsonExists) {
await migrateModelsJsonToYaml(jsonPath, yamlPath);
}
// Prefer models.yml, fall back to models.json
const primaryPath = existsSync(yamlPath) ? yamlPath : jsonPath;
const primaryPath = yamlExists ? yamlPath : jsonPath;
// Get all models config paths (user-level only), excluding the primary
const yamlPaths = getConfigDirPaths("models.yml", { project: false });
const jsonPaths = getConfigDirPaths("models.json", { project: false });
const allPaths = [...yamlPaths, ...jsonPaths];
const fallbackPaths = allPaths.filter((p) => p !== primaryPath && existsSync(p));
const existenceResults = await Promise.all(
allPaths.map((p) =>
Bun.file(p)
.exists()
.then((exists) => ({ p, exists })),
),
);
const fallbackPaths = existenceResults.filter(({ p, exists }) => p !== primaryPath && exists).map(({ p }) => p);
logger.debug("discoverModels", { primaryPath, fallbackPaths });
const registry = new ModelRegistry(authStorage, primaryPath, fallbackPaths);
registry.refresh();
await registry.refresh();
return registry;
}
@@ -319,7 +325,7 @@ async function migrateModelsJsonToYaml(jsonPath: string, yamlPath: string): Prom
return;
}
await Bun.write(yamlPath, YAML.stringify(parsed, null, 2));
await rename(jsonPath, `${jsonPath}.bak`);
await fs.rename(jsonPath, `${jsonPath}.bak`);
logger.debug("migrateModelsJsonToYaml: migrated models.json to models.yml", { from: jsonPath, to: yamlPath });
} catch (error) {
logger.warn("migrateModelsJsonToYaml: migration failed", { error: String(error) });
@@ -13,7 +13,6 @@
* Modes use this class and add their own I/O layer on top.
*/
import { existsSync, readFileSync } from "node:fs";
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type {
AssistantMessage,
@@ -26,7 +25,7 @@ import type {
UsageReport,
} from "@oh-my-pi/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
import { abortableSleep, logger } from "@oh-my-pi/pi-utils";
import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
import type { Rule } from "../capability/rule";
import { getAgentDbPath } from "../config";
@@ -214,9 +213,9 @@ const noOpUIContext: ExtensionUIContext = {
get theme() {
return theme;
},
getAllThemes: () => [],
getTheme: () => undefined,
setTheme: (_theme) => ({ success: false, error: "UI not available" }),
getAllThemes: () => Promise.resolve([]),
getTheme: () => Promise.resolve(undefined),
setTheme: (_theme) => Promise.resolve({ success: false, error: "UI not available" }),
setFooter: () => {},
setHeader: () => {},
setEditorComponent: () => {},
@@ -599,20 +598,18 @@ export class AgentSession {
if (!path) return;
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
this._ensureFileCache(resolvedPath);
void this._ensureFileCache(resolvedPath);
}
private _ensureFileCache(resolvedPath: string): void {
private async _ensureFileCache(resolvedPath: string): Promise<void> {
if (this._streamingEditFileCache.has(resolvedPath)) return;
try {
if (existsSync(resolvedPath)) {
const rawText = readFileSync(resolvedPath, "utf8");
const { text } = stripBom(rawText);
this._streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
}
const rawText = await Bun.file(resolvedPath).text();
const { text } = stripBom(rawText);
this._streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
} catch {
// Don't cache on read errors - let the edit tool handle them
// Don't cache on read errors (including ENOENT) - let the edit tool handle them
}
}
@@ -701,7 +698,6 @@ export class AgentSession {
): Promise<void> {
if (this._streamingEditAbortTriggered) return;
try {
if (!(await Bun.file(resolvedPath).exists())) return;
const { text } = stripBom(await Bun.file(resolvedPath).text());
const normalizedContent = normalizeToLF(text);
const missing = removedLines.find((line) => !normalizedContent.includes(normalizeToLF(line)));
@@ -714,8 +710,12 @@ export class AgentSession {
});
this.agent.abort();
}
} catch {
// Ignore errors during async fallback
} catch (err) {
// Ignore ENOENT (file not found) - let the edit tool handle missing files
// Also ignore other errors during async fallback
if (!isEnoent(err)) {
// Log unexpected errors but don't abort
}
}
}
@@ -2022,17 +2022,15 @@ export class AgentSession {
if (!sessionFile) return;
const todoPath = `${sessionFile.slice(0, -6)}/todos.json`;
const file = Bun.file(todoPath);
if (!(await file.exists())) {
this._todoReminderCount = 0;
return;
}
let todos: TodoItem[];
try {
const data = await file.json();
const data = await Bun.file(todoPath).json();
todos = data?.todos ?? [];
} catch {
} catch (err) {
if (isEnoent(err)) {
this._todoReminderCount = 0;
}
return;
}
@@ -1,6 +1,6 @@
import { Database } from "bun:sqlite";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { getAgentDbPath } from "../config";
import type { Settings } from "../config/settings-manager";
@@ -484,20 +484,20 @@ CREATE TABLE settings (
* @param dbPath - Path to the database file
*/
private ensureDir(dbPath: string): void {
mkdirSync(dirname(dbPath), { recursive: true });
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
}
private hardenPermissions(dbPath: string): void {
const dir = dirname(dbPath);
const dir = path.dirname(dbPath);
try {
chmodSync(dir, 0o700);
fs.chmodSync(dir, 0o700);
} catch (error) {
logger.warn("AgentStorage failed to chmod agent dir", { path: dir, error: String(error) });
}
if (!existsSync(dbPath)) return;
if (!fs.existsSync(dbPath)) return;
try {
chmodSync(dbPath, 0o600);
fs.chmodSync(dbPath, 0o600);
} catch (error) {
logger.warn("AgentStorage failed to chmod db file", { path: dbPath, error: String(error) });
}
@@ -5,8 +5,8 @@
* accessible via artifact:// URLs or the $ARTIFACTS environment variable.
*/
import { mkdir, readdir } from "node:fs/promises";
import { join } from "node:path";
import * as fs from "node:fs/promises";
import * as path from "node:path";
/**
* Manages artifact storage for a session.
@@ -38,7 +38,7 @@ export class ArtifactManager {
async #ensureDir(): Promise<void> {
if (!this.#dirCreated) {
await mkdir(this.#dir, { recursive: true });
await fs.mkdir(this.#dir, { recursive: true });
this.#dirCreated = true;
}
if (!this.#initialized) {
@@ -82,7 +82,7 @@ export class ArtifactManager {
await this.#ensureDir();
const id = String(this.allocateId());
const filename = `${id}.${toolType}.log`;
return { id, path: join(this.#dir, filename) };
return { id, path: path.join(this.#dir, filename) };
}
/**
@@ -113,7 +113,7 @@ export class ArtifactManager {
*/
async listFiles(): Promise<string[]> {
try {
return await readdir(this.#dir);
return await fs.readdir(this.#dir);
} catch {
return [];
}
@@ -128,6 +128,6 @@ export class ArtifactManager {
async getPath(id: string): Promise<string | null> {
const files = await this.listFiles();
const match = files.find((f) => f.startsWith(`${id}.`));
return match ? join(this.#dir, match) : null;
return match ? path.join(this.#dir, match) : null;
}
}
@@ -4,7 +4,7 @@
*/
import { Buffer } from "node:buffer";
import { dirname, join } from "node:path";
import * as path from "node:path";
import {
antigravityUsageProvider,
claudeUsageProvider,
@@ -282,7 +282,7 @@ export class AuthStorage {
if (authPath.endsWith(".db")) {
return authPath;
}
return getAgentDbPath(dirname(authPath));
return getAgentDbPath(path.dirname(authPath));
}
/**
@@ -313,10 +313,10 @@ export class AuthStorage {
* Migrates legacy auth.json/settings.json on first load.
*/
async reload(): Promise<void> {
const agentDir = dirname(this.dbPath);
const agentDir = path.dirname(this.dbPath);
await migrateJsonStorage({
agentDir,
settingsPath: join(agentDir, "settings.json"),
settingsPath: path.join(agentDir, "settings.json"),
authPaths: [this.authPath, ...this.fallbackPaths],
});
@@ -1,6 +1,6 @@
import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import { getAgentDir } from "../config";
@@ -81,7 +81,7 @@ END;
this.lastPromptCache = last?.prompt ?? null;
}
static open(dbPath: string = join(getAgentDir(), "history.db")): HistoryStorage {
static open(dbPath: string = path.join(getAgentDir(), "history.db")): HistoryStorage {
if (!HistoryStorage.instance) {
HistoryStorage.instance = new HistoryStorage(dbPath);
}
@@ -134,8 +134,8 @@ END;
}
private ensureDir(dbPath: string): void {
const dir = dirname(dbPath);
mkdirSync(dir, { recursive: true });
const dir = path.dirname(dbPath);
fs.mkdirSync(dir, { recursive: true });
}
private normalizeLimit(limit: number): number {
@@ -1,7 +1,7 @@
import { basename, join, resolve } from "node:path";
import * as path from "node:path";
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
import { logger } from "@oh-my-pi/pi-utils";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { nanoid } from "nanoid";
import { getAgentDir as getDefaultAgentDir } from "../config";
import { resizeImage } from "../utils/image-resize";
@@ -432,16 +432,24 @@ export function buildSessionContext(
*/
function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
const sessionDir = join(getDefaultAgentDir(), "sessions", safePath);
const sessionDir = path.join(getDefaultAgentDir(), "sessions", safePath);
storage.ensureDirSync(sessionDir);
return sessionDir;
}
/** Exported for testing */
export function loadEntriesFromFile(filePath: string, storage: SessionStorage = new FileSessionStorage()): FileEntry[] {
if (!storage.existsSync(filePath)) return [];
export async function loadEntriesFromFile(
filePath: string,
storage: SessionStorage = new FileSessionStorage(),
): Promise<FileEntry[]> {
let content: string;
try {
content = await storage.readText(filePath);
} catch (err) {
if (isEnoent(err)) return [];
throw err;
}
const content = storage.readTextSync(filePath);
const entries: FileEntry[] = [];
const lines = content.trim().split("\n");
@@ -555,15 +563,13 @@ function extractFirstUserPrompt(lines: string[]): string | undefined {
* Uses low-level file I/O to efficiently read only the first 4KB of each file
* to extract the JSON header and first user message without loading entire session logs into memory.
*/
function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentSessionInfo[] {
async function getSortedSessions(sessionDir: string, storage: SessionStorage): Promise<RecentSessionInfo[]> {
try {
const buf = Buffer.alloc(4096);
const files: string[] = storage.listFilesSync(sessionDir, "*.jsonl");
return files
.map((path: string) => {
const results = await Promise.all(
files.map(async (path: string) => {
try {
const length = storage.readTextPrefixSync(path, buf);
const content = buf.toString("utf-8", 0, length);
const content = await storage.readTextPrefix(path, 4096);
const lines = content.split("\n");
const firstLine = lines[0];
if (!firstLine || !firstLine.trim()) return null;
@@ -575,20 +581,20 @@ function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentS
} catch {
return null;
}
})
.filter((item): item is RecentSessionInfo => item !== null)
.sort((a, b) => b.mtime - a.mtime);
}),
);
return results.filter((item): item is RecentSessionInfo => item !== null).sort((a, b) => b.mtime - a.mtime);
} catch {
return [];
}
}
/** Exported for testing */
export function findMostRecentSession(
export async function findMostRecentSession(
sessionDir: string,
storage: SessionStorage = new FileSessionStorage(),
): string | null {
const sessions = getSortedSessions(sessionDir, storage);
): Promise<string | null> {
const sessions = await getSortedSessions(sessionDir, storage);
return sessions[0]?.path || null;
}
@@ -843,12 +849,13 @@ class NdjsonFileWriter {
}
/** Get recent sessions for display in welcome screen */
export function getRecentSessions(
export async function getRecentSessions(
sessionDir: string,
limit = 3,
storage: SessionStorage = new FileSessionStorage(),
): RecentSessionInfo[] {
return getSortedSessions(sessionDir, storage).slice(0, limit);
): Promise<RecentSessionInfo[]> {
const sessions = await getSortedSessions(sessionDir, storage);
return sessions.slice(0, limit);
}
/**
@@ -886,12 +893,12 @@ function extractTextFromContent(content: Message["content"]): string {
.join(" ");
}
function collectSessionsFromFiles(files: string[], storage: SessionStorage): SessionInfo[] {
async function collectSessionsFromFiles(files: string[], storage: SessionStorage): Promise<SessionInfo[]> {
const sessions: SessionInfo[] = [];
for (const file of files) {
try {
const content = storage.readTextSync(file);
const content = await storage.readText(file);
const lines = content.trim().split("\n");
if (lines.length === 0) continue;
@@ -1003,9 +1010,9 @@ export class SessionManager {
await this._closePersistWriter();
this.persistError = undefined;
this.persistErrorReported = false;
this.sessionFile = resolve(sessionFile);
if (this.storage.existsSync(this.sessionFile)) {
this.fileEntries = loadEntriesFromFile(this.sessionFile!, this.storage);
this.sessionFile = path.resolve(sessionFile);
this.fileEntries = await loadEntriesFromFile(this.sessionFile, this.storage);
if (this.fileEntries.length > 0) {
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
this.sessionId = header?.id ?? nanoid();
this.sessionTitle = header?.title;
@@ -1053,7 +1060,7 @@ export class SessionManager {
if (this.persist) {
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
this.sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
}
return this.sessionFile;
}
@@ -1154,8 +1161,8 @@ export class SessionManager {
private async _writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
if (!this.sessionFile) return;
const dir = resolve(this.sessionFile, "..");
const tempPath = join(dir, `.${basename(this.sessionFile)}.${nanoid(6)}.tmp`);
const dir = path.resolve(this.sessionFile, "..");
const tempPath = path.join(dir, `.${path.basename(this.sessionFile)}.${nanoid(6)}.tmp`);
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
try {
for (const entry of entries) {
@@ -1709,18 +1716,18 @@ export class SessionManager {
* Returns the new session file path, or undefined if not persisting.
*/
createBranchedSession(leafId: string): string | undefined {
const path = this.getBranch(leafId);
if (path.length === 0) {
const branchPath = this.getBranch(leafId);
if (branchPath.length === 0) {
throw new Error(`Entry ${leafId} not found`);
}
// Filter out LabelEntry from path - we'll recreate them from the resolved map
const pathWithoutLabels = path.filter((e) => e.type !== "label");
const pathWithoutLabels = branchPath.filter((e) => e.type !== "label");
const newSessionId = nanoid();
const timestamp = new Date().toISOString();
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
const newSessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
const header: SessionHeader = {
type: "session",
@@ -1816,7 +1823,7 @@ export class SessionManager {
): Promise<SessionManager> {
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
const manager = new SessionManager(cwd, dir, true, storage);
const forkEntries = structuredClone(loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
migrateToCurrentVersion(forkEntries);
const sourceHeader = forkEntries.find((e) => e.type === "session") as SessionHeader | undefined;
const historyEntries = forkEntries.filter((entry) => entry.type !== "session") as SessionEntry[];
@@ -1836,18 +1843,18 @@ export class SessionManager {
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
*/
static async open(
path: string,
filePath: string,
sessionDir?: string,
storage: SessionStorage = new FileSessionStorage(),
): Promise<SessionManager> {
// Extract cwd from session header if possible, otherwise use process.cwd()
const entries = loadEntriesFromFile(path, storage);
const entries = await loadEntriesFromFile(filePath, storage);
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
const cwd = header?.cwd ?? process.cwd();
// If no sessionDir provided, derive from file's parent directory
const dir = sessionDir ?? resolve(path, "..");
const dir = sessionDir ?? path.resolve(filePath, "..");
const manager = new SessionManager(cwd, dir, true, storage);
await manager._initSessionFile(path);
await manager._initSessionFile(filePath);
return manager;
}
@@ -1862,7 +1869,7 @@ export class SessionManager {
storage: SessionStorage = new FileSessionStorage(),
): Promise<SessionManager> {
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
const mostRecent = findMostRecentSession(dir, storage);
const mostRecent = await findMostRecentSession(dir, storage);
const manager = new SessionManager(cwd, dir, true, storage);
if (mostRecent) {
await manager._initSessionFile(mostRecent);
@@ -1884,11 +1891,15 @@ export class SessionManager {
* @param cwd Working directory (used to compute default session directory)
* @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
*/
static list(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionInfo[] {
static async list(
cwd: string,
sessionDir?: string,
storage: SessionStorage = new FileSessionStorage(),
): Promise<SessionInfo[]> {
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
try {
const files = storage.listFilesSync(dir, "*.jsonl");
return collectSessionsFromFiles(files, storage);
return await collectSessionsFromFiles(files, storage);
} catch {
return [];
}
@@ -1897,13 +1908,13 @@ export class SessionManager {
/**
* List all sessions across all project directories.
*/
static listAll(storage: SessionStorage = new FileSessionStorage()): SessionInfo[] {
const sessionsRoot = join(getDefaultAgentDir(), "sessions");
static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
try {
const files = Array.from(new Bun.Glob("**/*.jsonl").scanSync(sessionsRoot)).map((name) =>
join(sessionsRoot, name),
path.join(sessionsRoot, name),
);
return collectSessionsFromFiles(files, storage);
return await collectSessionsFromFiles(files, storage);
} catch {
return [];
}
@@ -1,17 +1,7 @@
import {
closeSync,
existsSync,
fsyncSync,
mkdirSync,
openSync,
readFileSync,
readSync,
statSync,
writeFileSync,
writeSync,
} from "node:fs";
import { rename as renameAsync } from "node:fs/promises";
import { dirname, join } from "node:path";
import * as fs from "node:fs";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
export interface SessionStorageStat {
size: number;
@@ -30,14 +20,13 @@ export interface SessionStorageWriter {
export interface SessionStorage {
ensureDirSync(dir: string): void;
existsSync(path: string): boolean;
readTextSync(path: string): string;
readTextPrefixSync(path: string, buf: Buffer): number;
writeTextSync(path: string, content: string): void;
statSync(path: string): SessionStorageStat;
listFilesSync(dir: string, pattern: string): string[];
exists(path: string): Promise<boolean>;
readText(path: string): Promise<string>;
readTextPrefix(path: string, maxBytes: number): Promise<string>;
writeText(path: string, content: string): Promise<void>;
rename(path: string, nextPath: string): Promise<void>;
unlink(path: string): Promise<void>;
@@ -52,7 +41,7 @@ function toError(value: unknown): Error {
// FinalizationRegistry to clean up leaked file descriptors
const writerRegistry = new FinalizationRegistry<number>((fd) => {
try {
closeSync(fd);
fs.closeSync(fd);
} catch {
// Ignore - fd may already be closed or invalid
}
@@ -64,16 +53,16 @@ class FileSessionStorageWriter implements SessionStorageWriter {
private error: Error | undefined;
private onError: ((err: Error) => void) | undefined;
constructor(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
this.onError = options?.onError;
const flags = options?.flags ?? "a";
// Ensure parent directory exists
const dir = dirname(path);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
const dir = path.dirname(fpath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Open file once, keep fd for lifetime
this.fd = openSync(path, flags === "w" ? "w" : "a");
this.fd = fs.openSync(fpath, flags === "w" ? "w" : "a");
// Register for cleanup if abandoned without close()
writerRegistry.register(this, this.fd, this);
}
@@ -92,7 +81,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
const buf = Buffer.from(line, "utf-8");
let offset = 0;
while (offset < buf.length) {
const written = writeSync(this.fd, buf, offset, buf.length - offset);
const written = fs.writeSync(this.fd, buf, offset, buf.length - offset);
if (written === 0) {
throw new Error("Short write");
}
@@ -112,7 +101,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
if (this.closed) throw new Error("Writer closed");
if (this.error) throw this.error;
try {
fsyncSync(this.fd);
fs.fsyncSync(this.fd);
} catch (err) {
throw this.recordError(err);
}
@@ -124,7 +113,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
// Unregister from finalization - we're closing properly
writerRegistry.unregister(this);
try {
closeSync(this.fd);
fs.closeSync(this.fd);
} catch {
// Ignore close errors
}
@@ -137,53 +126,56 @@ class FileSessionStorageWriter implements SessionStorageWriter {
export class FileSessionStorage implements SessionStorage {
ensureDirSync(dir: string): void {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
existsSync(path: string): boolean {
return existsSync(path);
return fs.existsSync(path);
}
readTextSync(path: string): string {
return readFileSync(path, "utf-8");
}
readTextPrefixSync(path: string, buf: Buffer): number {
const fd = openSync(path, "r");
try {
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
return bytesRead;
} finally {
closeSync(fd);
}
}
writeTextSync(path: string, content: string): void {
this.ensureDirSync(dirname(path));
writeFileSync(path, content);
writeTextSync(fpath: string, content: string): void {
this.ensureDirSync(path.dirname(fpath));
fs.writeFileSync(fpath, content);
}
statSync(path: string): SessionStorageStat {
const stats = statSync(path);
const stats = fs.statSync(path);
return { size: stats.size, mtimeMs: stats.mtimeMs, mtime: stats.mtime };
}
listFilesSync(dir: string, pattern: string): string[] {
try {
return Array.from(new Bun.Glob(pattern).scanSync(dir)).map((name) => join(dir, name));
return Array.from(new Bun.Glob(pattern).scanSync(dir)).map((name) => path.join(dir, name));
} catch {
return [];
}
}
exists(path: string): Promise<boolean> {
return Bun.file(path).exists();
async exists(path: string): Promise<boolean> {
try {
await fs.promises.access(path);
return true;
} catch (err) {
if (isEnoent(err)) return false;
throw err;
}
}
readText(path: string): Promise<string> {
return Bun.file(path).text();
return fs.promises.readFile(path, "utf-8");
}
async readTextPrefix(path: string, maxBytes: number): Promise<string> {
const handle = await fs.promises.open(path, "r");
try {
const buffer = Buffer.alloc(maxBytes);
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
return buffer.subarray(0, bytesRead).toString("utf-8");
} finally {
await handle.close();
}
}
async writeText(path: string, content: string): Promise<void> {
@@ -192,23 +184,23 @@ export class FileSessionStorage implements SessionStorage {
async rename(path: string, nextPath: string): Promise<void> {
try {
await renameAsync(path, nextPath);
await fs.promises.rename(path, nextPath);
} catch (err) {
throw toError(err);
}
}
unlink(path: string): Promise<void> {
return Bun.file(path).unlink();
return fs.promises.unlink(path);
}
fsyncDirSync(dir: string): void {
try {
const fd = openSync(dir, "r");
const fd = fs.openSync(dir, "r");
try {
fsyncSync(fd);
fs.fsyncSync(fd);
} finally {
closeSync(fd);
fs.closeSync(fd);
}
} catch {
// Best-effort: some platforms/filesystems don't support fsync on directories.
@@ -265,7 +257,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
await this.ready;
if (this.error) throw this.error;
try {
const existing = this.storage.existsSync(this.path) ? this.storage.readTextSync(this.path) : "";
const existing = this.storage.existsSync(this.path) ? await this.storage.readText(this.path) : "";
await this.storage.writeText(this.path, `${existing}${line}`);
} catch (err) {
throw this.recordError(err);
@@ -305,17 +297,6 @@ export class MemorySessionStorage implements SessionStorage {
return this.files.has(path);
}
readTextSync(path: string): string {
const entry = this.files.get(path);
if (!entry) throw new Error(`File not found: ${path}`);
return entry.content;
}
readTextPrefixSync(path: string, buf: Buffer): number {
const content = this.readTextSync(path);
return buf.write(content, 0, buf.length, "utf-8");
}
writeTextSync(path: string, content: string): void {
this.files.set(path, { content, mtimeMs: Date.now() });
}
@@ -348,7 +329,15 @@ export class MemorySessionStorage implements SessionStorage {
}
readText(path: string): Promise<string> {
return Promise.resolve(this.readTextSync(path));
const entry = this.files.get(path);
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
return Promise.resolve(entry.content);
}
readTextPrefix(path: string, maxBytes: number): Promise<string> {
const entry = this.files.get(path);
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
return Promise.resolve(entry.content.slice(0, maxBytes));
}
writeText(path: string, content: string): Promise<void> {
@@ -1,8 +1,9 @@
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
import * as fs from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import * as path from "node:path";
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
import { $ } from "bun";
import { CONFIG_DIR_NAME } from "../config";
export interface SSHConnectionTarget {
@@ -25,32 +26,28 @@ export interface SSHHostInfo {
compatEnabled: boolean;
}
const CONTROL_DIR = join(homedir(), CONFIG_DIR_NAME, "ssh-control");
const CONTROL_PATH = join(CONTROL_DIR, "%h.sock");
const HOST_INFO_DIR = join(homedir(), CONFIG_DIR_NAME, "remote-host");
const CONTROL_DIR = path.join(homedir(), CONFIG_DIR_NAME, "ssh-control");
const CONTROL_PATH = path.join(CONTROL_DIR, "%h.sock");
const HOST_INFO_DIR = path.join(homedir(), CONFIG_DIR_NAME, "remote-host");
const HOST_INFO_VERSION = 2;
const activeHosts = new Map<string, SSHConnectionTarget>();
const pendingConnections = new Map<string, Promise<void>>();
const hostInfoCache = new Map<string, SSHHostInfo>();
function ensureControlDir(): void {
if (!existsSync(CONTROL_DIR)) {
mkdirSync(CONTROL_DIR, { recursive: true, mode: 0o700 });
}
async function ensureControlDir(): Promise<void> {
await fs.mkdir(CONTROL_DIR, { recursive: true, mode: 0o700 });
try {
chmodSync(CONTROL_DIR, 0o700);
await fs.chmod(CONTROL_DIR, 0o700);
} catch (err) {
logger.debug("SSH control dir chmod failed", { path: CONTROL_DIR, error: String(err) });
}
}
function ensureHostInfoDir(): void {
if (!existsSync(HOST_INFO_DIR)) {
mkdirSync(HOST_INFO_DIR, { recursive: true, mode: 0o700 });
}
async function ensureHostInfoDir(): Promise<void> {
await fs.mkdir(HOST_INFO_DIR, { recursive: true, mode: 0o700 });
try {
chmodSync(HOST_INFO_DIR, 0o700);
await fs.chmod(HOST_INFO_DIR, 0o700);
} catch (err) {
logger.debug("SSH host info dir chmod failed", { path: HOST_INFO_DIR, error: String(err) });
}
@@ -62,15 +59,20 @@ function sanitizeHostName(name: string): string {
}
function getHostInfoPath(name: string): string {
return join(HOST_INFO_DIR, `${sanitizeHostName(name)}.json`);
return path.join(HOST_INFO_DIR, `${sanitizeHostName(name)}.json`);
}
function validateKeyPermissions(keyPath?: string): void {
async function validateKeyPermissions(keyPath?: string): Promise<void> {
if (!keyPath) return;
if (!existsSync(keyPath)) {
throw new Error(`SSH key not found: ${keyPath}`);
let stats: Awaited<ReturnType<typeof fs.stat>>;
try {
stats = await fs.stat(keyPath);
} catch (err) {
if (isEnoent(err)) {
throw new Error(`SSH key not found: ${keyPath}`);
}
throw err;
}
const stats = statSync(keyPath);
if (!stats.isFile()) {
throw new Error(`SSH key is not a file: ${keyPath}`);
}
@@ -208,31 +210,31 @@ function shouldRefreshHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): bo
return false;
}
function loadHostInfoFromDisk(host: SSHConnectionTarget): SSHHostInfo | undefined {
async function loadHostInfoFromDisk(host: SSHConnectionTarget): Promise<SSHHostInfo | undefined> {
const path = getHostInfoPath(host.name);
if (!existsSync(path)) return undefined;
try {
const raw = readFileSync(path, "utf-8");
const raw = await fs.readFile(path, "utf-8");
const parsed = parseHostInfo(JSON.parse(raw));
if (!parsed) return undefined;
const resolved = applyCompatOverride(host, parsed);
hostInfoCache.set(host.name, resolved);
return resolved;
} catch (err) {
if (isEnoent(err)) return undefined;
logger.warn("Failed to load SSH host info", { host: host.name, error: String(err) });
return undefined;
}
}
function loadHostInfoFromDiskByName(hostName: string): SSHHostInfo | undefined {
async function loadHostInfoFromDiskByName(hostName: string): Promise<SSHHostInfo | undefined> {
const path = getHostInfoPath(hostName);
if (!existsSync(path)) return undefined;
try {
const raw = readFileSync(path, "utf-8");
const raw = await fs.readFile(path, "utf-8");
const parsed = parseHostInfo(JSON.parse(raw));
if (!parsed) return undefined;
return parsed;
} catch (err) {
if (isEnoent(err)) return undefined;
logger.warn("Failed to load SSH host info", { host: hostName, error: String(err) });
return undefined;
}
@@ -240,7 +242,7 @@ function loadHostInfoFromDiskByName(hostName: string): SSHHostInfo | undefined {
async function persistHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): Promise<void> {
try {
ensureHostInfoDir();
await ensureHostInfoDir();
const path = getHostInfoPath(host.name);
const payload = { ...info, version: HOST_INFO_VERSION };
hostInfoCache.set(host.name, payload);
@@ -252,7 +254,7 @@ async function persistHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): Pr
async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
const command = 'echo "$OSTYPE|$SHELL|$BASH_VERSION" 2>/dev/null || echo "%OS%|%COMSPEC%|"';
const result = await runSshCaptureSync(buildRemoteCommand(host, command));
const result = await runSshCaptureSync(await buildRemoteCommand(host, command));
if (result.exitCode !== 0 && !result.stdout) {
logger.debug("SSH host probe failed", { host: host.name, error: result.stderr });
const fallback: SSHHostInfo = {
@@ -315,11 +317,11 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
const hasBash = !unexpandedPosixVars && (Boolean(bashVersion) || shell === "bash");
let compatShell: SSHHostInfo["compatShell"];
if (os === "windows" && host.compat !== false) {
const bashProbe = await runSshCaptureSync(buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"'));
const bashProbe = await runSshCaptureSync(await buildRemoteCommand(host, 'bash -lc "echo OMP_BASH_OK"'));
if (bashProbe.exitCode === 0 && bashProbe.stdout.includes("OMP_BASH_OK")) {
compatShell = "bash";
} else {
const shProbe = await runSshCaptureSync(buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"'));
const shProbe = await runSshCaptureSync(await buildRemoteCommand(host, 'sh -lc "echo OMP_SH_OK"'));
if (shProbe.exitCode === 0 && shProbe.stdout.includes("OMP_SH_OK")) {
compatShell = "sh";
}
@@ -344,18 +346,20 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
return info;
}
export function getHostInfo(hostName: string): SSHHostInfo | undefined {
return hostInfoCache.get(hostName) ?? loadHostInfoFromDiskByName(hostName);
export async function getHostInfo(hostName: string): Promise<SSHHostInfo | undefined> {
const cached = hostInfoCache.get(hostName);
if (cached) return cached;
return loadHostInfoFromDiskByName(hostName);
}
export function getHostInfoForHost(host: SSHConnectionTarget): SSHHostInfo | undefined {
export async function getHostInfoForHost(host: SSHConnectionTarget): Promise<SSHHostInfo | undefined> {
const cached = hostInfoCache.get(host.name);
if (cached) {
const resolved = applyCompatOverride(host, cached);
if (resolved !== cached) hostInfoCache.set(host.name, resolved);
return resolved;
}
return loadHostInfoFromDisk(host);
return await loadHostInfoFromDisk(host);
}
export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
@@ -365,7 +369,7 @@ export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHost
hostInfoCache.set(host.name, resolved);
if (!shouldRefreshHostInfo(host, resolved)) return resolved;
}
const fromDisk = loadHostInfoFromDisk(host);
const fromDisk = await loadHostInfoFromDisk(host);
if (fromDisk && !shouldRefreshHostInfo(host, fromDisk)) return fromDisk;
await ensureConnection(host);
const current = hostInfoCache.get(host.name);
@@ -373,8 +377,8 @@ export async function ensureHostInfo(host: SSHConnectionTarget): Promise<SSHHost
return probeHostInfo(host);
}
export function buildRemoteCommand(host: SSHConnectionTarget, command: string): string[] {
validateKeyPermissions(host.keyPath);
export async function buildRemoteCommand(host: SSHConnectionTarget, command: string): Promise<string[]> {
await validateKeyPermissions(host.keyPath);
return [...buildCommonArgs(host), buildSshTarget(host), command];
}
@@ -388,14 +392,14 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
const promise = (async () => {
ensureSshBinary();
ensureControlDir();
validateKeyPermissions(host.keyPath);
await ensureControlDir();
await validateKeyPermissions(host.keyPath);
const target = buildSshTarget(host);
const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]);
if (check.exitCode === 0) {
activeHosts.set(key, host);
if (!hostInfoCache.has(key) && !loadHostInfoFromDisk(host)) {
if (!hostInfoCache.has(key) && !(await loadHostInfoFromDisk(host))) {
await probeHostInfo(host);
}
return;
@@ -408,7 +412,7 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
}
activeHosts.set(key, host);
if (!hostInfoCache.has(key) && !loadHostInfoFromDisk(host)) {
if (!hostInfoCache.has(key) && !(await loadHostInfoFromDisk(host))) {
await probeHostInfo(host);
}
})();
@@ -76,7 +76,7 @@ export async function executeSSH(
}
}
const child = cspawn(["ssh", ...buildRemoteCommand(host, resolvedCommand)], {
const child = cspawn(["ssh", ...(await buildRemoteCommand(host, resolvedCommand))], {
signal: options?.signal,
timeout: options?.timeout,
});
+9 -16
View File
@@ -1,25 +1,21 @@
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import * as fs from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { logger } from "@oh-my-pi/pi-utils";
import * as path from "node:path";
import { $ } from "bun";
import { CONFIG_DIR_NAME } from "../config";
import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager";
const REMOTE_DIR = join(homedir(), CONFIG_DIR_NAME, "remote");
const REMOTE_DIR = path.join(homedir(), CONFIG_DIR_NAME, "remote");
const CONTROL_DIR = getControlDir();
const CONTROL_PATH = getControlPathTemplate();
const mountedPaths = new Set<string>();
function ensureDir(path: string, mode = 0o700): void {
if (!existsSync(path)) {
mkdirSync(path, { recursive: true, mode });
}
async function ensureDir(path: string, mode = 0o700): Promise<void> {
try {
chmodSync(path, mode);
} catch (err) {
logger.debug("SSHFS dir chmod failed", { path, error: String(err) });
await fs.promises.mkdir(path, { recursive: true, mode });
} catch {
await fs.promises.chmod(path, mode).catch((e) => void e);
}
}
@@ -30,7 +26,7 @@ function getMountName(host: SSHConnectionTarget): string {
}
function getMountPath(host: SSHConnectionTarget): string {
return join(REMOTE_DIR, getMountName(host));
return path.join(REMOTE_DIR, getMountName(host));
}
function buildSshTarget(host: SSHConnectionTarget): string {
@@ -95,11 +91,8 @@ export async function isMounted(path: string): Promise<boolean> {
export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"): Promise<string | undefined> {
if (!hasSshfs()) return undefined;
ensureDir(REMOTE_DIR);
ensureDir(CONTROL_DIR);
const mountPath = getMountPath(host);
ensureDir(mountPath);
await Promise.all([ensureDir(REMOTE_DIR), ensureDir(CONTROL_DIR), ensureDir(mountPath)]);
if (await isMounted(mountPath)) {
mountedPaths.add(mountPath);
+5 -5
View File
@@ -2,9 +2,8 @@
* System prompt construction and project context loading
*/
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import * as path from "node:path";
import { $ } from "bun";
import chalk from "chalk";
import { contextFileCapability } from "./capability/context-file";
@@ -430,14 +429,15 @@ interface SystemInfoCache {
}
function getSystemInfoCachePath(): string {
return join(homedir(), ".omp", "system_info.json");
return path.join(homedir(), ".omp", "system_info.json");
}
async function loadSystemInfoCache(): Promise<SystemInfoCache | null> {
try {
const cachePath = getSystemInfoCachePath();
if (!existsSync(cachePath)) return null;
const content = await Bun.file(cachePath).json();
const file = Bun.file(cachePath);
if (!(await file.exists())) return null;
const content = await file.json();
return content as SystemInfoCache;
} catch {
return null;

Some files were not shown because too many files have changed in this diff Show More