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:
@@ -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"])` |
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -47,6 +47,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sinclair/typebox": "^0.34.46",
|
||||
"@types/node": "^24.3.0"
|
||||
"@types/node": "^25.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" } }),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
`);
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ?? []);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user