diff --git a/crates/pi-natives/src/glob_util.rs b/crates/pi-natives/src/glob_util.rs index f05387d2c..4ce59b548 100644 --- a/crates/pi-natives/src/glob_util.rs +++ b/crates/pi-natives/src/glob_util.rs @@ -1,7 +1,7 @@ //! Shared glob-pattern helpers used by both [`crate::glob`] and //! [`crate::grep`]. -use globset::{Glob, GlobSet, GlobSetBuilder}; +use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; use napi::bindgen_prelude::*; /// Normalize a raw glob string: fix path separators, optionally prepend `**/` @@ -23,7 +23,9 @@ pub fn build_glob_pattern(glob: &str, recursive: bool) -> String { pub fn compile_glob(glob: &str, recursive: bool) -> Result { let mut builder = GlobSetBuilder::new(); let pattern = build_glob_pattern(glob, recursive); - let glob = Glob::new(&pattern) + let glob = GlobBuilder::new(&pattern) + .literal_separator(true) + .build() .map_err(|err| Error::from_reason(format!("Invalid glob pattern: {err}")))?; builder.add(glob); builder diff --git a/docs/skills.md b/docs/skills.md index 5c010ee94..994630fd2 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -30,7 +30,7 @@ For provider-based discovery (native/Claude/Codex/Agents/plugin providers), skil Nested patterns like `/group//SKILL.md` are not discovered by provider loaders. -For `skills.customDirectories`, scanning is recursive and treats any directory containing `SKILL.md` as a skill root. +For `skills.customDirectories`, scanning uses the same non-recursive layout (`*/SKILL.md`). ```text Provider-discovered layout (non-recursive under skills/): @@ -44,7 +44,7 @@ Provider-discovered layout (non-recursive under skills/): └─ internal/ └─ SKILL.md ❌ not discovered by provider loaders -Custom-directory scanning is recursive, so the same nested path is valid when that parent is listed in `skills.customDirectories`. +Custom-directory scanning is also non-recursive, so nested paths are ignored unless you point `customDirectories` at that nested parent. ``` @@ -63,15 +63,15 @@ Current runtime behavior: - `name` defaults to the skill directory name - `description` is required for: - native `.omp` provider skill discovery (`requireDescription: true`) - - `skills.customDirectories` scan in `extensibility/skills.ts` + - `skills.customDirectories` scans via `scanSkillsFromDir` in `src/discovery/helpers.ts` (non-recursive) - non-native providers can load skills without description ## Discovery pipeline -`loadSkills()` in `src/extensibility/skills.ts` does two passes: +`discoverSkills()` in `src/extensibility/skills.ts` does two passes: 1. **Capability providers** via `loadCapability("skills")` -2. **Custom directories** via recursive scan of `skills.customDirectories` +2. **Custom directories** via `scanSkillsFromDir(..., { requireDescription: true })` (one-level directory enumeration) If `skills.enabled` is `false`, discovery returns no skills. @@ -92,7 +92,7 @@ Dedup key is skill name. First item with a given name wins. ### Source toggles and filtering -`loadSkills()` applies these controls: +`discoverSkills()` applies these controls: - source toggles: `enableCodexUser`, `enableClaudeUser`, `enableClaudeProject`, `enablePiUser`, `enablePiProject` - glob filters on skill name: @@ -113,6 +113,7 @@ For providers other than codex/claude/native (for example `agents`, `claude-plug - `extensibility/skills.ts` additionally: - de-duplicates identical files by `realpath` (symlink-safe) - emits collision warnings when a later skill name conflicts + - keeps the convenience `discoverSkillsFromDir({ dir, source })` API as a thin adapter over `scanSkillsFromDir` - Custom-directory skills are merged after provider skills and follow the same collision behavior ## Runtime usage behavior @@ -214,5 +215,5 @@ No fallback search is performed for missing assets. - Put each skill in its own directory: `//SKILL.md` - Always include explicit `name` and `description` frontmatter - Keep referenced assets under the same skill directory and access with `skill:///...` -- If you need nested taxonomy (`team/domain/skill`), use `skills.customDirectories` (recursive scanner), not provider `skills/` roots +- For nested taxonomy (`team/domain/skill`), point `skills.customDirectories` to the nested parent directory; scanning itself remains non-recursive - Avoid duplicate skill names across sources; first match wins by provider precedence diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index e811b45fb..d1542fab2 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -10,8 +10,7 @@ import { Database, type Statement } from "bun:sqlite"; import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, logger } from "@oh-my-pi/pi-utils"; import { googleGeminiCliUsageProvider } from "./providers/google-gemini-cli-usage"; import { getEnvApiKey } from "./stream"; import type { Provider } from "./types"; diff --git a/packages/ai/src/model-cache.ts b/packages/ai/src/model-cache.ts index d2fa0b548..ded4aa39f 100644 --- a/packages/ai/src/model-cache.ts +++ b/packages/ai/src/model-cache.ts @@ -4,7 +4,7 @@ */ import { Database } from "bun:sqlite"; import * as path from "node:path"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir } from "@oh-my-pi/pi-utils"; import type { Api, Model } from "./types"; const CACHE_SCHEMA_VERSION = 2; diff --git a/packages/ai/src/utils/anthropic-auth.ts b/packages/ai/src/utils/anthropic-auth.ts index 3c97400b7..ec0f4fbf3 100644 --- a/packages/ai/src/utils/anthropic-auth.ts +++ b/packages/ai/src/utils/anthropic-auth.ts @@ -6,8 +6,7 @@ * 2. OAuth credentials in ~/.omp/agent/agent.db (with expiry check) * 3. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback */ -import { $env } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getAgentDbPath } from "@oh-my-pi/pi-utils"; import { type AuthCredential, AuthCredentialStore } from "../auth-storage"; import { buildAnthropicHeaders as buildProviderAnthropicHeaders } from "../providers/anthropic"; import { getEnvApiKey } from "../stream"; diff --git a/packages/ai/src/utils/oauth/kimi.ts b/packages/ai/src/utils/oauth/kimi.ts index 1b3730974..99cf8d06f 100644 --- a/packages/ai/src/utils/oauth/kimi.ts +++ b/packages/ai/src/utils/oauth/kimi.ts @@ -6,8 +6,7 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; -import { $env, abortableSleep, isEnoent } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, abortableSleep, getAgentDir, isEnoent } from "@oh-my-pi/pi-utils"; import packageJson from "../../../package.json" with { type: "json" }; import type { OAuthController, OAuthCredentials } from "./types"; diff --git a/packages/ai/test/oauth.ts b/packages/ai/test/oauth.ts index 93df3d7c9..c4b9f113f 100644 --- a/packages/ai/test/oauth.ts +++ b/packages/ai/test/oauth.ts @@ -9,8 +9,7 @@ import * as fs from "node:fs/promises"; import { getOAuthApiKey } from "@oh-my-pi/pi-ai/utils/oauth"; import type { OAuthCredentials, OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getTestAuthPath } from "@oh-my-pi/pi-utils/dirs"; +import { getTestAuthPath, isEnoent } from "@oh-my-pi/pi-utils"; /** * E2E tests require explicit opt-in via E2E=1 environment variable. diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 0ef713096..13e285514 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -5,8 +5,7 @@ import { streamOpenAICodexResponses, } from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; import type { Context, Model, ProviderSessionState } from "@oh-my-pi/pi-ai/types"; -import { TempDir } from "@oh-my-pi/pi-utils"; -import { getAgentDir, setAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, setAgentDir, TempDir } from "@oh-my-pi/pi-utils"; const originalFetch = global.fetch; const originalAgentDir = getAgentDir(); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 11f8dcc54..280313485 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,22 @@ - Made `description` field required in CustomTool interface +### Changed + +- Reorganized imports from `@oh-my-pi/pi-utils/dirs` to consolidate with main `@oh-my-pi/pi-utils` exports for cleaner dependency management +- Renamed `loadSkillsFromDir` to `scanSkillsFromDir` with updated interface for improved clarity on skill discovery behavior +- Moved `tryParseJson` utility from local scrapers module to `@oh-my-pi/pi-utils` for centralized JSON parsing +- Simplified patch module exports by consolidating type re-exports with `export * from './types'` +- Removed `emitCustomToolSessionEvent` method from AgentSession for streamlined session lifecycle management +- Changed skill discovery from recursive to non-recursive (one level deep only) for improved performance and clarity +- Simplified skill loading logic by removing recursive directory traversal and consolidating ignore rule handling + +### Removed + +- Removed `parseJSON` helper function from discovery module (replaced by `tryParseJson` from pi-utils) +- Removed backwards compatibility comment from `AskToolDetails.question` field +- Removed unused SSH resource cleanup functions `closeAllConnections` and `unmountAll` from session imports + ## [13.1.2] - 2026-02-23 ### Breaking Changes diff --git a/packages/coding-agent/DEVELOPMENT.md b/packages/coding-agent/DEVELOPMENT.md index 4698000b0..e02010b39 100644 --- a/packages/coding-agent/DEVELOPMENT.md +++ b/packages/coding-agent/DEVELOPMENT.md @@ -623,14 +623,14 @@ Normalization and collision handling: - `source = ":",` - preserves `_source`. -Custom directory loading (separate path): +Custom directory loading (delegated scanner): -- `customDirectories` are scanned directly via `scanDirectoryForSkills`, not via capability providers. -- Recursive scan respects `.gitignore` / `.ignore` / `.fdignore` through `addIgnoreRules` + `shouldIgnore`. -- Directory containing `SKILL.md` is treated as one skill root and is not recursed further. +- `customDirectories` are scanned via `scanSkillsFromDir` from `src/discovery/helpers.ts` (not ad-hoc traversal in `extensibility/skills.ts`). +- Custom directory scans are non-recursive (`*/SKILL.md`). +- Scan uses one-level `readdir` candidate enumeration (`*/SKILL.md`) without recursive descent. - Custom skills are stamped as `source: "custom:user"` with `_source.provider = "custom"`. -Also exported: `loadSkillsFromDir({ dir, source })` for direct recursive loading of skill directories with ignore-file support. +Also exported: `discoverSkillsFromDir({ dir, source })`, which delegates directly to `scanSkillsFromDir` with non-recursive scanning. ## MCP Manager, LSP Client Boundary, and Internal URL Routing diff --git a/packages/coding-agent/src/capability/index.ts b/packages/coding-agent/src/capability/index.ts index 0eff9ae7b..6f202ca87 100644 --- a/packages/coding-agent/src/capability/index.ts +++ b/packages/coding-agent/src/capability/index.ts @@ -8,8 +8,7 @@ */ import * as os from "node:os"; import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, logger } from "@oh-my-pi/pi-utils"; import type { Settings } from "../config/settings"; import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs"; diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 3429fdcc3..6a0986c14 100755 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -1,10 +1,10 @@ #!/usr/bin/env bun +import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils"; /** * CLI entry point — registers all commands explicitly and delegates to the * lightweight CLI runner from pi-utils. */ import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli"; -import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils/dirs"; // Detect known Bun errata that cause TUI crashes (e.g. Bun.stringWidth mishandling OSC sequences). if (Bun.stringWidth("\x1b[0m\x1b]8;;\x07") !== 0) { diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 6cc242ba3..e692f7b6d 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -2,8 +2,7 @@ * CLI argument parsing and help display */ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; -import { logger } from "@oh-my-pi/pi-utils"; -import { APP_NAME, CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { BUILTIN_TOOLS } from "../tools"; diff --git a/packages/coding-agent/src/cli/config-cli.ts b/packages/coding-agent/src/cli/config-cli.ts index 2b979ae8e..b3c66e317 100644 --- a/packages/coding-agent/src/cli/config-cli.ts +++ b/packages/coding-agent/src/cli/config-cli.ts @@ -5,7 +5,7 @@ * Uses settings-defs as the source of truth for available settings. */ -import { APP_NAME, getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getAgentDir } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { getDefault, diff --git a/packages/coding-agent/src/cli/file-processor.ts b/packages/coding-agent/src/cli/file-processor.ts index b81791aaf..489a504c4 100644 --- a/packages/coding-agent/src/cli/file-processor.ts +++ b/packages/coding-agent/src/cli/file-processor.ts @@ -4,8 +4,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { ImageContent } from "@oh-my-pi/pi-ai"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, isEnoent } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { resolveReadPath } from "../tools/path-utils"; import { formatBytes } from "../tools/render-utils"; diff --git a/packages/coding-agent/src/cli/grep-cli.ts b/packages/coding-agent/src/cli/grep-cli.ts index 72f3b719d..f7118c66a 100644 --- a/packages/coding-agent/src/cli/grep-cli.ts +++ b/packages/coding-agent/src/cli/grep-cli.ts @@ -5,7 +5,7 @@ */ import * as path from "node:path"; import { grep } from "@oh-my-pi/pi-natives"; -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; export interface GrepCommandArgs { diff --git a/packages/coding-agent/src/cli/jupyter-cli.ts b/packages/coding-agent/src/cli/jupyter-cli.ts index ba5a0861a..ff129488e 100644 --- a/packages/coding-agent/src/cli/jupyter-cli.ts +++ b/packages/coding-agent/src/cli/jupyter-cli.ts @@ -4,7 +4,7 @@ * Handles `omp jupyter` subcommand for managing the shared Python gateway. */ -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { getGatewayStatus, shutdownSharedGateway } from "../ipy/gateway-coordinator"; diff --git a/packages/coding-agent/src/cli/plugin-cli.ts b/packages/coding-agent/src/cli/plugin-cli.ts index 5f2897d66..bb4cf3a34 100644 --- a/packages/coding-agent/src/cli/plugin-cli.ts +++ b/packages/coding-agent/src/cli/plugin-cli.ts @@ -4,7 +4,7 @@ * Handles `omp plugin ` subcommands for plugin lifecycle management. */ -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins"; import { theme } from "../modes/theme/theme"; diff --git a/packages/coding-agent/src/cli/setup-cli.ts b/packages/coding-agent/src/cli/setup-cli.ts index 38aa50462..316f51334 100644 --- a/packages/coding-agent/src/cli/setup-cli.ts +++ b/packages/coding-agent/src/cli/setup-cli.ts @@ -4,7 +4,7 @@ * Handles `omp setup ` to install dependencies for optional features. */ import * as path from "node:path"; -import { APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import chalk from "chalk"; import { theme } from "../modes/theme/theme"; diff --git a/packages/coding-agent/src/cli/shell-cli.ts b/packages/coding-agent/src/cli/shell-cli.ts index 87a85dfd5..6c87cd643 100644 --- a/packages/coding-agent/src/cli/shell-cli.ts +++ b/packages/coding-agent/src/cli/shell-cli.ts @@ -6,7 +6,7 @@ import * as path from "node:path"; import { createInterface } from "node:readline/promises"; import { Shell } from "@oh-my-pi/pi-natives"; -import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { Settings } from "../config/settings"; import { getOrCreateSnapshot } from "../utils/shell-snapshot"; diff --git a/packages/coding-agent/src/cli/ssh-cli.ts b/packages/coding-agent/src/cli/ssh-cli.ts index 212e28d09..0cdb54570 100644 --- a/packages/coding-agent/src/cli/ssh-cli.ts +++ b/packages/coding-agent/src/cli/ssh-cli.ts @@ -4,7 +4,7 @@ * Handles `omp ssh ` subcommands for SSH host configuration management. */ -import { getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs"; +import { getSSHConfigPath } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { addSSHHost, readSSHConfigFile, removeSSHHost, type SSHHostConfig } from "../ssh/config-writer"; diff --git a/packages/coding-agent/src/cli/stats-cli.ts b/packages/coding-agent/src/cli/stats-cli.ts index 0363e1eb8..1c62ef8bb 100644 --- a/packages/coding-agent/src/cli/stats-cli.ts +++ b/packages/coding-agent/src/cli/stats-cli.ts @@ -4,8 +4,7 @@ * Handles `omp stats` subcommand for viewing AI usage statistics. */ -import { formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils"; -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { openPath } from "../utils/open"; diff --git a/packages/coding-agent/src/cli/update-cli.ts b/packages/coding-agent/src/cli/update-cli.ts index 672277aee..41432c4c8 100644 --- a/packages/coding-agent/src/cli/update-cli.ts +++ b/packages/coding-agent/src/cli/update-cli.ts @@ -7,8 +7,7 @@ import { execSync, spawnSync } from "node:child_process"; import * as fs from "node:fs"; import { pipeline } from "node:stream/promises"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { theme } from "../modes/theme/theme"; diff --git a/packages/coding-agent/src/cli/web-search-cli.ts b/packages/coding-agent/src/cli/web-search-cli.ts index 1f11cc078..d4c9680bc 100644 --- a/packages/coding-agent/src/cli/web-search-cli.ts +++ b/packages/coding-agent/src/cli/web-search-cli.ts @@ -4,7 +4,7 @@ * Handles `omp q`/`omp web-search` subcommands for testing web search providers. */ -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { initTheme, theme } from "../modes/theme/theme"; import { runSearchQuery, type SearchParams } from "../web/search/index"; diff --git a/packages/coding-agent/src/commands/launch.ts b/packages/coding-agent/src/commands/launch.ts index 27184f1c5..c4b63b76d 100644 --- a/packages/coding-agent/src/commands/launch.ts +++ b/packages/coding-agent/src/commands/launch.ts @@ -1,8 +1,9 @@ /** * Root command for the coding agent CLI. */ + +import { APP_NAME } from "@oh-my-pi/pi-utils"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; import { parseArgs } from "../cli/args"; import { runRootCommand } from "../main"; diff --git a/packages/coding-agent/src/commit/agentic/index.ts b/packages/coding-agent/src/commit/agentic/index.ts index ca317d961..e2e647517 100644 --- a/packages/coding-agent/src/commit/agentic/index.ts +++ b/packages/coding-agent/src/commit/agentic/index.ts @@ -1,7 +1,6 @@ import * as path from "node:path"; import { createInterface } from "node:readline/promises"; -import { $env, isEnoent } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils"; import { applyChangelogProposals } from "../../commit/changelog"; import { detectChangelogBoundaries } from "../../commit/changelog/detect"; import { parseUnreleasedSection } from "../../commit/changelog/parse"; diff --git a/packages/coding-agent/src/commit/pipeline.ts b/packages/coding-agent/src/commit/pipeline.ts index 631a8d976..ddc35cc4b 100644 --- a/packages/coding-agent/src/commit/pipeline.ts +++ b/packages/coding-agent/src/commit/pipeline.ts @@ -1,7 +1,6 @@ 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 { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, logger } from "@oh-my-pi/pi-utils"; import { ModelRegistry } from "../config/model-registry"; import { renderPromptTemplate } from "../config/prompt-templates"; import { Settings } from "../config/settings"; diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 657bf2407..02534cc1d 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -1,8 +1,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { CONFIG_DIR_NAME, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { CONFIG_DIR_NAME, getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils"; import type { TSchema } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; import { Ajv, type ErrorObject, type ValidateFunction } from "ajv"; diff --git a/packages/coding-agent/src/config/keybindings.ts b/packages/coding-agent/src/config/keybindings.ts index d1337a9f8..574b71533 100644 --- a/packages/coding-agent/src/config/keybindings.ts +++ b/packages/coding-agent/src/config/keybindings.ts @@ -8,8 +8,7 @@ import { matchesKey, setEditorKeybindings, } from "@oh-my-pi/pi-tui"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils"; /** * Application-level actions (coding agent specific). diff --git a/packages/coding-agent/src/config/prompt-templates.ts b/packages/coding-agent/src/config/prompt-templates.ts index 66171fa2c..ea708ce06 100644 --- a/packages/coding-agent/src/config/prompt-templates.ts +++ b/packages/coding-agent/src/config/prompt-templates.ts @@ -1,7 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; -import { getProjectDir, getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, getProjectPromptsDir, getPromptsDir, logger } from "@oh-my-pi/pi-utils"; import Handlebars from "handlebars"; import { computeLineHash } from "../patch/hashline"; import { jtdToTypeScript } from "../tools/jtd-to-typescript"; diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index 95fb49615..61acae85c 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -13,8 +13,15 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { isEnoent, logger, procmgr, setDefaultTabWidth } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { + getAgentDbPath, + getAgentDir, + getProjectDir, + isEnoent, + logger, + procmgr, + setDefaultTabWidth, +} from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings"; import type { ModelRole } from "../config/model-registry"; diff --git a/packages/coding-agent/src/debug/index.ts b/packages/coding-agent/src/debug/index.ts index 8123c033f..e528aa73b 100644 --- a/packages/coding-agent/src/debug/index.ts +++ b/packages/coding-agent/src/debug/index.ts @@ -7,7 +7,7 @@ import * as fs from "node:fs/promises"; import * as url from "node:url"; import { getWorkProfile } from "@oh-my-pi/pi-natives"; import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui"; -import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs"; +import { getSessionsDir } from "@oh-my-pi/pi-utils"; import { DynamicBorder } from "../modes/components/dynamic-border"; import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme"; import type { InteractiveModeContext } from "../modes/types"; diff --git a/packages/coding-agent/src/debug/report-bundle.ts b/packages/coding-agent/src/debug/report-bundle.ts index 767ffcf44..635babe57 100644 --- a/packages/coding-agent/src/debug/report-bundle.ts +++ b/packages/coding-agent/src/debug/report-bundle.ts @@ -6,8 +6,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import type { WorkProfile } from "@oh-my-pi/pi-natives"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { APP_NAME, getLogPath, getLogsDir, getReportsDir } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getLogPath, getLogsDir, getReportsDir, isEnoent } from "@oh-my-pi/pi-utils"; import type { CpuProfile, HeapSnapshot } from "./profiler"; import { collectSystemInfo, sanitizeEnv } from "./system-info"; diff --git a/packages/coding-agent/src/debug/system-info.ts b/packages/coding-agent/src/debug/system-info.ts index 839b984f9..a77053251 100644 --- a/packages/coding-agent/src/debug/system-info.ts +++ b/packages/coding-agent/src/debug/system-info.ts @@ -3,8 +3,7 @@ */ import * as os from "node:os"; -import { formatBytes } from "@oh-my-pi/pi-utils"; -import { getProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs"; +import { formatBytes, getProjectDir, VERSION } from "@oh-my-pi/pi-utils"; export interface SystemInfo { os: string; diff --git a/packages/coding-agent/src/discovery/agents.ts b/packages/coding-agent/src/discovery/agents.ts index 2ef29ba31..9296901a2 100644 --- a/packages/coding-agent/src/discovery/agents.ts +++ b/packages/coding-agent/src/discovery/agents.ts @@ -13,7 +13,7 @@ import { type Skill, skillCapability } from "../capability/skill"; import { type SlashCommand, slashCommandCapability } from "../capability/slash-command"; import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt"; import type { LoadContext, LoadResult } from "../capability/types"; -import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, loadSkillsFromDir } from "./helpers"; +import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, scanSkillsFromDir } from "./helpers"; const PROVIDER_ID = "agents"; const DISPLAY_NAME = "Agents (standard)"; @@ -28,7 +28,7 @@ async function loadSkills(ctx: LoadContext): Promise> { const items: Skill[] = []; const warnings: string[] = []; for (const userSkillsDir of getUserAgentPathCandidates(ctx, "skills")) { - const result = await loadSkillsFromDir(ctx, { + const result = await scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user", diff --git a/packages/coding-agent/src/discovery/builtin.ts b/packages/coding-agent/src/discovery/builtin.ts index 7257e5640..f0515d6f6 100644 --- a/packages/coding-agent/src/discovery/builtin.ts +++ b/packages/coding-agent/src/discovery/builtin.ts @@ -4,7 +4,7 @@ * Primary provider for OMP native configs. Supports all capabilities. */ import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; +import { logger, tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { type ContextFile, contextFileCapability } from "../capability/context-file"; import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension"; @@ -30,9 +30,8 @@ import { expandEnvVarsDeep, getExtensionNameFromPath, loadFilesFromDir, - loadSkillsFromDir, - parseJSON, SOURCE_PATHS, + scanSkillsFromDir, } from "./helpers"; const PROVIDER_ID = "native"; @@ -98,7 +97,7 @@ async function loadMCPServers(ctx: LoadContext): Promise> const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => { const result: MCPServer[] = []; - const data = parseJSON<{ mcpServers?: Record }>(content); + const data = tryParseJson<{ mcpServers?: Record }>(content); if (!data?.mcpServers) return result; const expanded = expandEnvVarsDeep(data.mcpServers); @@ -245,7 +244,7 @@ async function loadSkills(ctx: LoadContext): Promise> { const configDirs = await getConfigDirs(ctx); const results = await Promise.all( configDirs.map(({ dir, level }) => - loadSkillsFromDir(ctx, { + scanSkillsFromDir(ctx, { dir: path.join(dir, "skills"), providerId: PROVIDER_ID, level, @@ -404,7 +403,7 @@ async function loadExtensionModules(ctx: LoadContext): Promise(settingsContent); + const settingsData = tryParseJson<{ extensions?: unknown }>(settingsContent); const extensions = settingsData?.extensions; if (!Array.isArray(extensions)) continue; @@ -508,7 +507,7 @@ async function loadExtensions(ctx: LoadContext): Promise> if (!content) continue; const { extDir, manifestPath, entryName, level } = manifestCandidates[i]; - const manifest = parseJSON(content); + const manifest = tryParseJson(content); if (!manifest) { warnings.push(`Failed to parse ${manifestPath}`); continue; @@ -655,7 +654,7 @@ async function loadTools(ctx: LoadContext): Promise> { extensions: ["json", "md", "ts", "js", "sh", "bash", "py"], transform: (name, content, path, source) => { if (name.endsWith(".json")) { - const data = parseJSON<{ name?: string; description?: string }>(content); + const data = tryParseJson<{ name?: string; description?: string }>(content); const toolName = data?.name || name.replace(/\.json$/, ""); const description = typeof data?.description === "string" && data.description.trim() @@ -754,7 +753,7 @@ async function loadSettings(ctx: LoadContext): Promise> { const content = await readFile(settingsPath); if (!content) continue; - const data = parseJSON>(content); + const data = tryParseJson>(content); if (!data) { warnings.push(`Failed to parse ${settingsPath}`); continue; diff --git a/packages/coding-agent/src/discovery/claude-plugins.ts b/packages/coding-agent/src/discovery/claude-plugins.ts index c945d0c62..ec81f64ab 100644 --- a/packages/coding-agent/src/discovery/claude-plugins.ts +++ b/packages/coding-agent/src/discovery/claude-plugins.ts @@ -11,7 +11,7 @@ import { type Skill, skillCapability } from "../capability/skill"; import { type SlashCommand, slashCommandCapability } from "../capability/slash-command"; import { type CustomTool, toolCapability } from "../capability/tool"; import type { LoadContext, LoadResult } from "../capability/types"; -import { type ClaudePluginRoot, listClaudePluginRoots, loadFilesFromDir, loadSkillsFromDir } from "./helpers"; +import { type ClaudePluginRoot, listClaudePluginRoots, loadFilesFromDir, scanSkillsFromDir } from "./helpers"; const PROVIDER_ID = "claude-plugins"; const DISPLAY_NAME = "Claude Code Marketplace"; @@ -31,7 +31,7 @@ async function loadSkills(ctx: LoadContext): Promise> { const results = await Promise.all( roots.map(async root => { const skillsDir = path.join(root.path, "skills"); - return loadSkillsFromDir(ctx, { + return scanSkillsFromDir(ctx, { dir: skillsDir, providerId: PROVIDER_ID, level: root.scope, diff --git a/packages/coding-agent/src/discovery/claude.ts b/packages/coding-agent/src/discovery/claude.ts index bb01a023f..208f075e0 100644 --- a/packages/coding-agent/src/discovery/claude.ts +++ b/packages/coding-agent/src/discovery/claude.ts @@ -5,6 +5,7 @@ * Priority: 80 (tool-specific, below builtin but above shared standards) */ import * as path from "node:path"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { type ContextFile, contextFileCapability } from "../capability/context-file"; import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module"; @@ -24,8 +25,7 @@ import { expandEnvVarsDeep, getExtensionNameFromPath, loadFilesFromDir, - loadSkillsFromDir, - parseJSON, + scanSkillsFromDir, } from "./helpers"; const PROVIDER_ID = "claude"; @@ -77,7 +77,7 @@ async function loadMCPServers(ctx: LoadContext): Promise> const parseMcpServers = (content: string | null, path: string, level: "user" | "project"): MCPServer[] => { if (!content) return []; - const json = parseJSON<{ mcpServers?: Record }>(content); + const json = tryParseJson<{ mcpServers?: Record }>(content); if (!json?.mcpServers) return []; const mcpServers = expandEnvVarsDeep(json.mcpServers); @@ -163,8 +163,8 @@ async function loadSkills(ctx: LoadContext): Promise> { const projectSkillsDir = path.join(getProjectClaude(ctx), "skills"); const results = await Promise.all([ - loadSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }), - loadSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }), + scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }), + scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }), ]); return { @@ -396,7 +396,7 @@ async function loadSettings(ctx: LoadContext): Promise> { const userContent = await readFile(userSettingsJson); if (userContent) { - const data = parseJSON>(userContent); + const data = tryParseJson>(userContent); if (data) { items.push({ path: userSettingsJson, @@ -413,7 +413,7 @@ async function loadSettings(ctx: LoadContext): Promise> { const projectSettingsJson = path.join(projectBase, "settings.json"); const projectContent = await readFile(projectSettingsJson); if (projectContent) { - const data = parseJSON>(projectContent); + const data = tryParseJson>(projectContent); if (data) { items.push({ path: projectSettingsJson, diff --git a/packages/coding-agent/src/discovery/codex.ts b/packages/coding-agent/src/discovery/codex.ts index 3c5a61d9d..5d27d522a 100644 --- a/packages/coding-agent/src/discovery/codex.ts +++ b/packages/coding-agent/src/discovery/codex.ts @@ -35,8 +35,8 @@ import { discoverExtensionModulePaths, getExtensionNameFromPath, loadFilesFromDir, - loadSkillsFromDir, SOURCE_PATHS, + scanSkillsFromDir, } from "./helpers"; const PROVIDER_ID = "codex"; @@ -214,12 +214,12 @@ async function loadSkills(ctx: LoadContext): Promise> { const projectSkillsDir = path.join(codexDir, "skills"); const results = await Promise.all([ - loadSkillsFromDir(ctx, { + scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user", }), - loadSkillsFromDir(ctx, { + scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project", diff --git a/packages/coding-agent/src/discovery/cursor.ts b/packages/coding-agent/src/discovery/cursor.ts index 9313fa999..ceed627ba 100644 --- a/packages/coding-agent/src/discovery/cursor.ts +++ b/packages/coding-agent/src/discovery/cursor.ts @@ -13,6 +13,8 @@ * - rules: From rules/*.mdc files with MDC frontmatter (description, globs, alwaysApply) * - settings: From settings.json if present */ + +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { readFile } from "../capability/fs"; import { type MCPServer, mcpCapability } from "../capability/mcp"; @@ -28,7 +30,6 @@ import { getProjectPath, getUserPath, loadFilesFromDir, - parseJSON, } from "./helpers"; const PROVIDER_ID = "cursor"; @@ -46,7 +47,7 @@ function parseMCPServers( ): { items: MCPServer[]; warning?: string } { const items: MCPServer[] = []; - const parsed = parseJSON<{ mcpServers?: Record }>(content); + const parsed = tryParseJson<{ mcpServers?: Record }>(content); if (!parsed?.mcpServers) { return { items, warning: `${path}: missing or invalid 'mcpServers' key` }; } @@ -158,7 +159,7 @@ async function loadSettings(ctx: LoadContext): Promise> { const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null); if (userContent && userPath) { - const parsed = parseJSON>(userContent); + const parsed = tryParseJson>(userContent); if (parsed) { items.push({ path: userPath, @@ -173,7 +174,7 @@ async function loadSettings(ctx: LoadContext): Promise> { const projectContent = await projectContentPromise; if (projectContent && projectPath) { - const parsed = parseJSON>(projectContent); + const parsed = tryParseJson>(projectContent); if (parsed) { items.push({ path: projectPath, diff --git a/packages/coding-agent/src/discovery/gemini.ts b/packages/coding-agent/src/discovery/gemini.ts index 4546c2ca4..5cb211666 100644 --- a/packages/coding-agent/src/discovery/gemini.ts +++ b/packages/coding-agent/src/discovery/gemini.ts @@ -16,6 +16,7 @@ * - settings: From settings.json */ import * as path from "node:path"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { type ContextFile, contextFileCapability } from "../capability/context-file"; import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension"; @@ -33,7 +34,6 @@ import { getExtensionNameFromPath, getProjectPath, getUserPath, - parseJSON, } from "./helpers"; const PROVIDER_ID = "gemini"; @@ -80,7 +80,7 @@ async function loadMCPFromSettings( return { items, warnings }; } - const parsed = parseJSON<{ mcpServers?: Record }>(content); + const parsed = tryParseJson<{ mcpServers?: Record }>(content); if (!parsed) { warnings.push(`Invalid JSON in ${path}`); return { items, warnings }; @@ -206,7 +206,7 @@ async function loadExtensionsFromDir(extensionsDir: string, level: "user" | "pro for (const { entry, extPath, manifestPath, content } of results) { if (!content) continue; - const manifest = parseJSON(content); + const manifest = tryParseJson(content); if (!manifest) { warnings.push(`Invalid JSON in ${manifestPath}`); continue; @@ -268,7 +268,7 @@ async function loadSettings(ctx: LoadContext): Promise> { if (userPath) { const content = await readFile(userPath); if (content) { - const parsed = parseJSON>(content); + const parsed = tryParseJson>(content); if (parsed) { items.push({ path: userPath, @@ -287,7 +287,7 @@ async function loadSettings(ctx: LoadContext): Promise> { if (projectPath) { const content = await readFile(projectPath); if (content) { - const parsed = parseJSON>(content); + const parsed = tryParseJson>(content); if (parsed) { items.push({ path: projectPath, diff --git a/packages/coding-agent/src/discovery/helpers.ts b/packages/coding-agent/src/discovery/helpers.ts index 8c2aec75e..4320cf7e1 100644 --- a/packages/coding-agent/src/discovery/helpers.ts +++ b/packages/coding-agent/src/discovery/helpers.ts @@ -1,7 +1,8 @@ +import * as fs from "node:fs"; import * as path from "node:path"; import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { FileType, glob } from "@oh-my-pi/pi-natives"; -import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { CONFIG_DIR_NAME, tryParseJson } from "@oh-my-pi/pi-utils"; import { readFile } from "../capability/fs"; import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule"; import type { Skill, SkillFrontmatter } from "../capability/skill"; @@ -268,68 +269,56 @@ async function globIf( } } -export async function loadSkillsFromDir( +export interface ScanSkillsFromDirOptions { + dir: string; + providerId: string; + level: "user" | "project"; + requireDescription?: boolean; +} + +export async function scanSkillsFromDir( _ctx: LoadContext, - options: { - dir: string; - providerId: string; - level: "user" | "project"; - requireDescription?: boolean; - }, + options: ScanSkillsFromDirOptions, ): Promise> { const items: Skill[] = []; const warnings: string[] = []; const { dir, level, providerId, requireDescription = false } = options; - // Use native glob to find all SKILL.md files one level deep - // Pattern */SKILL.md matches //SKILL.md - const discoveredMatches = new Set(); - for (const match of await globIf(dir, "*/SKILL.md", FileType.File)) { - discoveredMatches.add(match.path); - } - for (const match of await globIf(dir, "*", FileType.Dir, false)) { - const skillRelPath = `${match.path}/SKILL.md`; - const content = await readFile(path.join(dir, skillRelPath)); - if (content !== null) { - discoveredMatches.add(skillRelPath); + + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + + const loadSkill = async (skillPath: string) => { + try { + const content = await readFile(skillPath); + if (!content) return; + const { frontmatter, body } = parseFrontmatter(content, { source: skillPath }); + if (requireDescription && !frontmatter.description) { + return; + } + const skillDirName = path.basename(path.dirname(skillPath)); + items.push({ + name: (frontmatter.name as string) || skillDirName, + path: skillPath, + content: body, + frontmatter: frontmatter as SkillFrontmatter, + level, + _source: createSourceMeta(providerId, skillPath, level), + }); + } catch { + warnings.push(`Failed to read skill file: ${skillPath}`); + } + }; + + const work = []; + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + const skillPath = path.join(dir, entry.name, "SKILL.md"); + if (fs.existsSync(skillPath)) { + work.push(loadSkill(skillPath)); } } - const matches = [...discoveredMatches].map(path => ({ path })); - if (matches.length === 0) { - return { items, warnings }; - } + await Promise.all(work); - // Read all skill files in parallel - const results = await Promise.all( - matches.map(async match => { - const skillFile = path.join(dir, match.path); - const content = await readFile(skillFile); - if (!content) { - return { item: null as Skill | null, warning: null as string | null }; - } - const { frontmatter, body } = parseFrontmatter(content, { source: skillFile }); - if (requireDescription && !frontmatter.description) { - return { item: null as Skill | null, warning: null as string | null }; - } - - // Extract skill name from path: "/SKILL.md" -> "" - const skillDirName = path.basename(path.dirname(skillFile)); - return { - item: { - name: (frontmatter.name as string) || skillDirName, - path: skillFile, - content: body, - frontmatter: frontmatter as SkillFrontmatter, - level, - _source: createSourceMeta(providerId, skillFile, level), - }, - warning: null as string | null, - }; - }), - ); - for (const result of results) { - if (result.warning) warnings.push(result.warning); - if (result.item) items.push(result.item); - } return { items, warnings }; } @@ -337,7 +326,7 @@ export async function loadSkillsFromDir( * Expand environment variables in a string. * Supports ${VAR} and ${VAR:-default} syntax. */ -export function expandEnvVars(value: string, extraEnv?: Record): string { +function expandEnvVars(value: string, extraEnv?: Record): string { return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => { const envValue = extraEnv?.[varName] ?? Bun.env[varName]; if (envValue !== undefined) return envValue; @@ -447,17 +436,6 @@ export async function loadFilesFromDir( return { items, warnings }; } -/** - * Parse JSON safely. - */ -export function parseJSON(content: string): T | null { - try { - return JSON.parse(content) as T; - } catch { - return null; - } -} - /** * Calculate depth of target directory relative to current working directory. * Depth is the number of directory levels from cwd to target. @@ -480,7 +458,7 @@ async function readExtensionModuleManifest( const content = await readFile(packageJsonPath); if (!content) return null; - const pkg = parseJSON<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content); + const pkg = tryParseJson<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content); const manifest = pkg?.omp ?? pkg?.pi; if (manifest && typeof manifest === "object") { return manifest; @@ -506,9 +484,9 @@ export async function discoverExtensionModulePaths(_ctx: LoadContext, dir: strin // 1. Direct *.ts or *.js files globIf(dir, "*.{ts,js}", FileType.File, false), // 2. Subdirectory index files - globIf(dir, "*/index.{ts,js}", FileType.File), + globIf(dir, "*/index.{ts,js}", FileType.File, false), // 3. Subdirectory package.json files - globIf(dir, "*/package.json", FileType.File), + globIf(dir, "*/package.json", FileType.File, false), ]); // Process direct files @@ -617,7 +595,7 @@ export interface ClaudePluginRoot { * Parse Claude Code installed_plugins.json content. */ export function parseClaudePluginsRegistry(content: string): ClaudePluginsRegistry | null { - const data = parseJSON(content); + const data = tryParseJson(content); if (!data || typeof data !== "object") return null; if ( typeof data.version !== "number" || diff --git a/packages/coding-agent/src/discovery/mcp-json.ts b/packages/coding-agent/src/discovery/mcp-json.ts index d094e2cb0..8128e59cb 100644 --- a/packages/coding-agent/src/discovery/mcp-json.ts +++ b/packages/coding-agent/src/discovery/mcp-json.ts @@ -7,12 +7,12 @@ * Priority: 5 (low, as this is a fallback after tool-specific providers) */ import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; +import { logger, tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { readFile } from "../capability/fs"; import { type MCPServer, mcpCapability } from "../capability/mcp"; import type { LoadContext, LoadResult, SourceMeta } from "../capability/types"; -import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers"; +import { createSourceMeta, expandEnvVarsDeep } from "./helpers"; const PROVIDER_ID = "mcp-json"; const DISPLAY_NAME = "MCP Config"; @@ -115,7 +115,7 @@ async function loadMCPJsonFile( return { items, warnings }; } - const config = parseJSON(content); + const config = tryParseJson(content); if (!config) { warnings.push(`Failed to parse JSON in ${path}`); return { items, warnings }; diff --git a/packages/coding-agent/src/discovery/opencode.ts b/packages/coding-agent/src/discovery/opencode.ts index d9db6aafa..dc490712a 100644 --- a/packages/coding-agent/src/discovery/opencode.ts +++ b/packages/coding-agent/src/discovery/opencode.ts @@ -16,7 +16,7 @@ * Priority: 55 (tool-specific provider) */ import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; +import { logger, tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { type ContextFile, contextFileCapability } from "../capability/context-file"; import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module"; @@ -35,8 +35,7 @@ import { getProjectPath, getUserPath, loadFilesFromDir, - loadSkillsFromDir, - parseJSON, + scanSkillsFromDir, } from "./helpers"; const PROVIDER_ID = "opencode"; @@ -51,7 +50,7 @@ async function loadJsonConfig(configPath: string): Promise>(content); + const parsed = tryParseJson>(content); if (!parsed) { logger.warn("Failed to parse OpenCode JSON config", { path: configPath }); return null; @@ -190,7 +189,7 @@ async function loadSkills(ctx: LoadContext): Promise> { if (userSkillsDir) { promises.push( - loadSkillsFromDir(ctx, { + scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user", @@ -200,7 +199,7 @@ async function loadSkills(ctx: LoadContext): Promise> { if (projectSkillsDir) { promises.push( - loadSkillsFromDir(ctx, { + scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project", @@ -307,7 +306,7 @@ async function loadSettings(ctx: LoadContext): Promise> { if (userConfigPath) { const content = await readFile(userConfigPath); if (content) { - const parsed = parseJSON>(content); + const parsed = tryParseJson>(content); if (parsed) { items.push({ path: userConfigPath, @@ -325,7 +324,7 @@ async function loadSettings(ctx: LoadContext): Promise> { const projectConfigPath = path.join(ctx.cwd, "opencode.json"); const content = await readFile(projectConfigPath); if (content) { - const parsed = parseJSON>(content); + const parsed = tryParseJson>(content); if (parsed) { items.push({ path: projectConfigPath, diff --git a/packages/coding-agent/src/discovery/ssh.ts b/packages/coding-agent/src/discovery/ssh.ts index 9b20011b3..6646c8629 100644 --- a/packages/coding-agent/src/discovery/ssh.ts +++ b/packages/coding-agent/src/discovery/ssh.ts @@ -5,13 +5,13 @@ * Priority: 5 (low, project/user config discovery) */ import * as path from "node:path"; -import { getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs"; +import { getSSHConfigPath, tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { readFile } from "../capability/fs"; import { type SSHHost, sshCapability } from "../capability/ssh"; import type { LoadContext, LoadResult, SourceMeta } from "../capability/types"; import { expandTilde } from "../tools/path-utils"; -import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers"; +import { createSourceMeta, expandEnvVarsDeep } from "./helpers"; const PROVIDER_ID = "ssh-json"; const DISPLAY_NAME = "SSH Config"; @@ -95,7 +95,7 @@ async function loadSshJsonFile( if (content === null) { return { items, warnings }; } - const parsed = parseJSON(content); + const parsed = tryParseJson(content); if (!parsed) { warnings.push(`Failed to parse JSON in ${filePath}`); return { items, warnings }; diff --git a/packages/coding-agent/src/discovery/vscode.ts b/packages/coding-agent/src/discovery/vscode.ts index 3ce6e3b09..63c584fa6 100644 --- a/packages/coding-agent/src/discovery/vscode.ts +++ b/packages/coding-agent/src/discovery/vscode.ts @@ -4,11 +4,12 @@ * Loads config from `.vscode` directory (project-only). * Supports MCP server discovery from `mcp.json` with nested `mcp.servers` structure. */ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { readFile } from "../capability/fs"; import { type MCPServer, mcpCapability } from "../capability/mcp"; import type { LoadContext, LoadResult } from "../capability/types"; -import { createSourceMeta, expandEnvVarsDeep, getProjectPath, parseJSON } from "./helpers"; +import { createSourceMeta, expandEnvVarsDeep, getProjectPath } from "./helpers"; const PROVIDER_ID = "vscode"; const DISPLAY_NAME = "VS Code"; @@ -57,7 +58,7 @@ async function loadMCPConfig( return { items, warnings }; } - const parsed = parseJSON<{ mcp?: { servers?: Record } }>(content); + const parsed = tryParseJson<{ mcp?: { servers?: Record } }>(content); if (!parsed) { warnings.push(`Invalid JSON in ${path}`); return { items, warnings }; diff --git a/packages/coding-agent/src/discovery/windsurf.ts b/packages/coding-agent/src/discovery/windsurf.ts index d3dbd2aca..1f0322d4a 100644 --- a/packages/coding-agent/src/discovery/windsurf.ts +++ b/packages/coding-agent/src/discovery/windsurf.ts @@ -10,6 +10,8 @@ * - Rules from .windsurf/rules/*.md and ~/.codeium/windsurf/memories/global_rules.md * - Legacy .windsurfrules file */ + +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import { readFile } from "../capability/fs"; import { type MCPServer, mcpCapability } from "../capability/mcp"; @@ -22,7 +24,6 @@ import { getProjectPath, getUserPath, loadFilesFromDir, - parseJSON, } from "./helpers"; const PROVIDER_ID = "windsurf"; @@ -78,7 +79,7 @@ async function loadMCPServers(ctx: LoadContext): Promise> for (const { content, path, scope } of configs) { if (!content || !path) continue; - const config = parseJSON<{ mcpServers?: Record }>(content); + const config = tryParseJson<{ mcpServers?: Record }>(content); if (!config?.mcpServers) continue; for (const [name, serverConfig] of Object.entries(config.mcpServers)) { diff --git a/packages/coding-agent/src/export/custom-share.ts b/packages/coding-agent/src/export/custom-share.ts index 1e9ec5254..14b994b0b 100644 --- a/packages/coding-agent/src/export/custom-share.ts +++ b/packages/coding-agent/src/export/custom-share.ts @@ -6,7 +6,7 @@ */ import * as fs from "node:fs"; import * as path from "node:path"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir } from "@oh-my-pi/pi-utils"; export interface CustomShareResult { /** URL to display/open (optional - script may handle everything itself) */ diff --git a/packages/coding-agent/src/export/html/index.ts b/packages/coding-agent/src/export/html/index.ts index 697aab72f..0c6b8576b 100644 --- a/packages/coding-agent/src/export/html/index.ts +++ b/packages/coding-agent/src/export/html/index.ts @@ -1,7 +1,6 @@ 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 "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, isEnoent } from "@oh-my-pi/pi-utils"; import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme"; import { type SessionEntry, type SessionHeader, SessionManager } from "../../session/session-manager"; // Pre-generated template (created by scripts/generate-template.ts at publish time) diff --git a/packages/coding-agent/src/extensibility/custom-commands/loader.ts b/packages/coding-agent/src/extensibility/custom-commands/loader.ts index d5e1f21f0..bb264da67 100644 --- a/packages/coding-agent/src/extensibility/custom-commands/loader.ts +++ b/packages/coding-agent/src/extensibility/custom-commands/loader.ts @@ -7,8 +7,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import * as piCodingAgent from "@oh-my-pi/pi-coding-agent"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils"; import * as typebox from "@sinclair/typebox"; import { getConfigDirs } from "../../config"; import { execCommand } from "../../exec/exec"; diff --git a/packages/coding-agent/src/extensibility/plugins/installer.ts b/packages/coding-agent/src/extensibility/plugins/installer.ts index 2fce52d0c..66649a2ea 100644 --- a/packages/coding-agent/src/extensibility/plugins/installer.ts +++ b/packages/coding-agent/src/extensibility/plugins/installer.ts @@ -1,7 +1,6 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils"; import { extractPackageName } from "./parser"; import type { InstalledPlugin } from "./types"; diff --git a/packages/coding-agent/src/extensibility/plugins/loader.ts b/packages/coding-agent/src/extensibility/plugins/loader.ts index ac91a7380..48d6bc43d 100644 --- a/packages/coding-agent/src/extensibility/plugins/loader.ts +++ b/packages/coding-agent/src/extensibility/plugins/loader.ts @@ -6,8 +6,7 @@ */ import * as fs from "node:fs"; import * as path from "node:path"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson } from "@oh-my-pi/pi-utils/dirs"; +import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson, isEnoent } from "@oh-my-pi/pi-utils"; import { getConfigDirPaths } from "../../config"; import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types"; diff --git a/packages/coding-agent/src/extensibility/plugins/manager.ts b/packages/coding-agent/src/extensibility/plugins/manager.ts index 26cdeb620..19e68814d 100644 --- a/packages/coding-agent/src/extensibility/plugins/manager.ts +++ b/packages/coding-agent/src/extensibility/plugins/manager.ts @@ -1,6 +1,5 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; import { getPluginsDir, getPluginsLockfile, @@ -8,7 +7,9 @@ import { getPluginsPackageJson, getProjectDir, getProjectPluginOverridesPath, -} from "@oh-my-pi/pi-utils/dirs"; + isEnoent, + logger, +} from "@oh-my-pi/pi-utils"; import { extractPackageName, parsePluginSpec } from "./parser"; import type { DoctorCheck, diff --git a/packages/coding-agent/src/extensibility/skills.ts b/packages/coding-agent/src/extensibility/skills.ts index da9f485a4..54e662bea 100644 --- a/packages/coding-agent/src/extensibility/skills.ts +++ b/packages/coding-agent/src/extensibility/skills.ts @@ -1,18 +1,12 @@ import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import * as os from "node:os"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; import { skillCapability } from "../capability/skill"; import type { SourceMeta } from "../capability/types"; import type { SkillsSettings } from "../config/settings"; -import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery"; -import { loadCapability } from "../discovery"; +import { type Skill as CapabilitySkill, loadCapability } from "../discovery"; +import { scanSkillsFromDir } from "../discovery/helpers"; import { expandTilde } from "../tools/path-utils"; -import { parseFrontmatter } from "../utils/frontmatter"; -import { addIgnoreRules, createIgnoreMatcher, type IgnoreMatcher, shouldIgnore } from "../utils/ignore-files"; - -// Re-export SkillFrontmatter for backward compatibility -export type { ImportedSkillFrontmatter as SkillFrontmatter }; export interface Skill { name: string; @@ -41,91 +35,31 @@ export interface LoadSkillsFromDirOptions { source: string; } -async function readFileContent(filePath: string): Promise { - try { - return await fs.readFile(filePath, "utf-8"); - } catch { - return null; - } -} - -/** - * Load skills from a directory recursively. - * Skills are directories containing a SKILL.md file with frontmatter including a description. - * Respects .gitignore, .ignore, and .fdignore files. - */ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Promise { - const skills: Skill[] = []; - const warnings: SkillWarning[] = []; - const seenPaths = new Set(); - const rootDir = options.dir; + const [rawProviderId, rawLevel] = options.source.split(":", 2); + const providerId = rawProviderId || "custom"; + const level: "user" | "project" = rawLevel === "project" ? "project" : "user"; + const result = await scanSkillsFromDir( + { cwd: getProjectDir(), home: os.homedir() }, + { + dir: options.dir, + providerId, + level, + requireDescription: true, + }, + ); - async function addSkill(skillFile: string, skillDir: string, dirName: string): Promise { - if (seenPaths.has(skillFile)) return; - try { - 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; - - if (description) { - seenPaths.add(skillFile); - skills.push({ - name, - description, - filePath: skillFile, - baseDir: skillDir, - source: options.source, - }); - } - } catch (error) { - logger.warn("Failed to load skill", { path: skillFile, error: String(error) }); - } - } - - async function scanDir(dir: string, ig: IgnoreMatcher): Promise { - try { - // Add ignore rules from this directory - await addIgnoreRules(ig, dir, rootDir, readFileContent); - - // First check if this directory itself is a skill - const selfSkillFile = path.join(dir, "SKILL.md"); - try { - const s = await fs.stat(selfSkillFile); - if (s.isFile()) { - await addSkill(selfSkillFile, dir, path.basename(dir)); - // This directory is a skill, don't recurse - return; - } - } catch { - // No SKILL.md in this directory - } - - // Recurse into subdirectories - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.name.startsWith(".") || entry.name === "node_modules") continue; - - const fullPath = path.join(dir, entry.name); - const isDir = entry.isDirectory(); - - // Check if this entry should be ignored - if (shouldIgnore(ig, rootDir, fullPath, isDir)) continue; - - if (isDir) { - await scanDir(fullPath, ig); - } - } - } catch (err) { - warnings.push({ skillPath: dir, message: `Failed to read directory: ${err}` }); - } - } - - const ig = createIgnoreMatcher(); - await scanDir(options.dir, ig); - - return { skills, warnings }; + return { + skills: result.items.map(capSkill => ({ + name: capSkill.name, + description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "", + filePath: capSkill.path, + baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""), + source: options.source, + _source: capSkill._source, + })), + warnings: (result.warnings ?? []).map(message => ({ skillPath: options.dir, message })), + }; } export interface LoadSkillsOptions extends SkillsSettings { @@ -225,45 +159,55 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise = []; - const customScanResults = await Promise.all( - customDirectories.map(dir => loadSkillsFromDir({ dir: expandTilde(dir), source: "custom" })), + const customDirectoryResults = await Promise.all( + customDirectories.map(async dir => { + const expandedDir = expandTilde(dir); + const scanResult = await scanSkillsFromDir( + { cwd, home: os.homedir() }, + { + dir: expandedDir, + providerId: "custom", + level: "user", + requireDescription: true, + }, + ); + return { expandedDir, scanResult }; + }), ); - for (const customSkills of customScanResults) { - for (const s of customSkills.skills) { - if (matchesIgnorePatterns(s.name)) continue; - if (!matchesIncludePatterns(s.name)) continue; + + const allCustomSkills: Array<{ skill: Skill; path: string }> = []; + for (const { expandedDir, scanResult } of customDirectoryResults) { + for (const capSkill of scanResult.items) { + if (matchesIgnorePatterns(capSkill.name)) continue; + if (!matchesIncludePatterns(capSkill.name)) continue; allCustomSkills.push({ skill: { - name: s.name, - description: s.description, - filePath: s.filePath, - baseDir: s.filePath.replace(/\/SKILL\.md$/, ""), + name: capSkill.name, + description: + typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "", + filePath: capSkill.path, + baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""), source: "custom:user", - _source: { provider: "custom", providerName: "Custom", path: s.filePath, level: "user" }, + _source: { ...capSkill._source, providerName: "Custom" }, }, - path: s.filePath, + path: capSkill.path, }); } - collisionWarnings.push(...customSkills.warnings); + collisionWarnings.push(...(scanResult.warnings ?? []).map(message => ({ skillPath: expandedDir, message }))); } - // Batch resolve custom skill paths const customRealPaths = await Promise.all( allCustomSkills.map(async ({ path }) => { try { @@ -293,6 +237,6 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise ({ skillPath: "", message: w })), ...collisionWarnings], + warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings], }; } diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 11b64f650..720c07c2e 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -6,8 +6,7 @@ export { StringEnum } from "@oh-my-pi/pi-ai"; // Re-export TUI components for custom tool rendering export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui"; // Logging -export { logger } from "@oh-my-pi/pi-utils"; -export { getAgentDir, VERSION } from "@oh-my-pi/pi-utils/dirs"; +export { getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils"; export { formatKeyHint, formatKeyHints } from "./config/keybindings"; export { ModelRegistry } from "./config/model-registry"; // Prompt templates @@ -89,7 +88,6 @@ export { loadSkills, loadSkillsFromDir, type Skill, - type SkillFrontmatter, type SkillWarning, } from "./extensibility/skills"; // Slash commands diff --git a/packages/coding-agent/src/ipy/executor.ts b/packages/coding-agent/src/ipy/executor.ts index b0bd7a5a3..1a8b9a023 100644 --- a/packages/coding-agent/src/ipy/executor.ts +++ b/packages/coding-agent/src/ipy/executor.ts @@ -1,6 +1,5 @@ import * as path from "node:path"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils"; import { OutputSink } from "../session/streaming-output"; import { shutdownSharedGateway } from "./gateway-coordinator"; import { diff --git a/packages/coding-agent/src/ipy/gateway-coordinator.ts b/packages/coding-agent/src/ipy/gateway-coordinator.ts index 814c353a5..b1ed845fe 100644 --- a/packages/coding-agent/src/ipy/gateway-coordinator.ts +++ b/packages/coding-agent/src/ipy/gateway-coordinator.ts @@ -1,8 +1,7 @@ import * as fs from "node:fs"; import { createServer } from "node:net"; import * as path from "node:path"; -import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils"; import type { Subprocess } from "bun"; import { Settings } from "../config/settings"; import { getOrCreateSnapshot } from "../utils/shell-snapshot"; diff --git a/packages/coding-agent/src/ipy/modules.ts b/packages/coding-agent/src/ipy/modules.ts index af023a172..55595a242 100644 --- a/packages/coding-agent/src/ipy/modules.ts +++ b/packages/coding-agent/src/ipy/modules.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { getAgentModulesDir, getProjectDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentModulesDir, getProjectDir, getProjectModulesDir } from "@oh-my-pi/pi-utils"; export type PythonModuleSource = "user" | "project"; diff --git a/packages/coding-agent/src/ipy/runtime.ts b/packages/coding-agent/src/ipy/runtime.ts index 4c4fd18c4..9498940c0 100644 --- a/packages/coding-agent/src/ipy/runtime.ts +++ b/packages/coding-agent/src/ipy/runtime.ts @@ -6,9 +6,7 @@ */ import * as fs from "node:fs"; import * as path from "node:path"; - -import { $env } from "@oh-my-pi/pi-utils"; -import { getPythonEnvDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getPythonEnvDir } from "@oh-my-pi/pi-utils"; const DEFAULT_ENV_ALLOWLIST = new Set([ "PATH", diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 0161ce606..f42615c72 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -11,8 +11,7 @@ import * as os from "node:os"; import * as path from "node:path"; import { createInterface } from "node:readline/promises"; import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai"; -import { $env, logger, postmortem } from "@oh-my-pi/pi-utils"; -import { getProjectDir, setProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getProjectDir, logger, postmortem, setProjectDir, VERSION } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import type { Args } from "./cli/args"; import { processFileArguments } from "./cli/file-processor"; diff --git a/packages/coding-agent/src/mcp/config.ts b/packages/coding-agent/src/mcp/config.ts index fd95c324d..3c67cffe6 100644 --- a/packages/coding-agent/src/mcp/config.ts +++ b/packages/coding-agent/src/mcp/config.ts @@ -4,7 +4,7 @@ * Uses the capability system to load MCP servers from multiple sources. */ -import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs"; +import { getMCPConfigPath } from "@oh-my-pi/pi-utils"; import { mcpCapability } from "../capability/mcp"; import type { SourceMeta } from "../capability/types"; import type { MCPServer } from "../discovery"; diff --git a/packages/coding-agent/src/mcp/transports/stdio.ts b/packages/coding-agent/src/mcp/transports/stdio.ts index 817fe94cc..43c15aa1d 100644 --- a/packages/coding-agent/src/mcp/transports/stdio.ts +++ b/packages/coding-agent/src/mcp/transports/stdio.ts @@ -5,8 +5,7 @@ * Messages are newline-delimited JSON. */ -import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, readJsonl, Snowflake } from "@oh-my-pi/pi-utils"; import { type Subprocess, spawn } from "bun"; import type { JsonRpcResponse, MCPRequestOptions, MCPStdioServerConfig, MCPTransport } from "../../mcp/types"; diff --git a/packages/coding-agent/src/memories/index.ts b/packages/coding-agent/src/memories/index.ts index da6e2c384..b6b95a628 100644 --- a/packages/coding-agent/src/memories/index.ts +++ b/packages/coding-agent/src/memories/index.ts @@ -4,8 +4,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import type { AgentMessage } from "@oh-my-pi/pi-agent-core"; import { completeSimple, type Model } from "@oh-my-pi/pi-ai"; -import { logger, parseJsonlLenient } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, logger, parseJsonlLenient } from "@oh-my-pi/pi-utils"; import type { ModelRegistry } from "../config/model-registry"; import { parseModelString } from "../config/model-resolver"; import { renderPromptTemplate } from "../config/prompt-templates"; diff --git a/packages/coding-agent/src/modes/components/footer.ts b/packages/coding-agent/src/modes/components/footer.ts index 8cbb8f1c0..d2cfded2e 100644 --- a/packages/coding-agent/src/modes/components/footer.ts +++ b/packages/coding-agent/src/modes/components/footer.ts @@ -1,7 +1,6 @@ import * as fs from "node:fs"; import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui"; -import { formatNumber } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils"; import { theme } from "../../modes/theme/theme"; import type { AgentSession } from "../../session/agent-session"; import { shortenPath } from "../../tools/render-utils"; diff --git a/packages/coding-agent/src/modes/components/status-line/segments.ts b/packages/coding-agent/src/modes/components/status-line/segments.ts index 1232466d0..92e6b225b 100644 --- a/packages/coding-agent/src/modes/components/status-line/segments.ts +++ b/packages/coding-agent/src/modes/components/status-line/segments.ts @@ -1,6 +1,5 @@ import * as os from "node:os"; -import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { formatDuration, formatNumber, getProjectDir } from "@oh-my-pi/pi-utils"; import { theme } from "../../../modes/theme/theme"; import { shortenPath } from "../../../tools/render-utils"; import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types"; diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index b74e58892..1ae25d2e1 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -13,17 +13,10 @@ import { Text, type TUI, } from "@oh-my-pi/pi-tui"; -import { logger } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, logger } from "@oh-my-pi/pi-utils"; import type { Theme } from "../../modes/theme/theme"; import { theme } from "../../modes/theme/theme"; -import { - computeEditDiff, - computeHashlineDiff, - computePatchDiff, - type EditDiffError, - type EditDiffResult, -} from "../../patch"; +import { computeEditDiff, computeHashlineDiff, computePatchDiff, type DiffError, type DiffResult } from "../../patch"; import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash"; import { formatArgsInline, @@ -106,7 +99,7 @@ export class ToolExecutionComponent extends Container { details?: any; }; // Cached edit diff preview (computed when args arrive, before tool executes) - #editDiffPreview?: EditDiffResult | EditDiffError; + #editDiffPreview?: DiffResult | DiffError; #editDiffArgsKey?: string; // Track which args the preview is for // Cached converted images for Kitty protocol (which requires PNG), keyed by index #convertedImages: Map = new Map(); diff --git a/packages/coding-agent/src/modes/components/welcome.ts b/packages/coding-agent/src/modes/components/welcome.ts index 2f43df2e6..6aa0802fd 100644 --- a/packages/coding-agent/src/modes/components/welcome.ts +++ b/packages/coding-agent/src/modes/components/welcome.ts @@ -1,5 +1,5 @@ import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui"; -import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME } from "@oh-my-pi/pi-utils"; import { theme } from "../../modes/theme/theme"; export interface RecentSession { diff --git a/packages/coding-agent/src/modes/controllers/command-controller.ts b/packages/coding-agent/src/modes/controllers/command-controller.ts index e90c092f1..cc3413c45 100644 --- a/packages/coding-agent/src/modes/controllers/command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/command-controller.ts @@ -10,8 +10,7 @@ import { } from "@oh-my-pi/pi-ai"; import { copyToClipboard } from "@oh-my-pi/pi-natives"; import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui"; -import { formatDuration, Snowflake } from "@oh-my-pi/pi-utils"; -import { setProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { formatDuration, Snowflake, setProjectDir } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import { reset as resetCapabilities } from "../../capability"; import { loadCustomShare } from "../../export/custom-share"; diff --git a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts index 48a65993c..3d3f76cb4 100644 --- a/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/mcp-command-controller.ts @@ -4,7 +4,7 @@ * Handles /mcp subcommands for managing MCP servers. */ import { Spacer, Text } from "@oh-my-pi/pi-tui"; -import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils"; import type { SourceMeta } from "../../capability/types"; import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp"; import { connectToServer, disconnectServer, listTools } from "../../mcp/client"; diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 2c92ae162..704818c09 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -2,7 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui"; -import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils"; import { MODEL_ROLES } from "../../config/model-registry"; import { settings } from "../../config/settings"; import { DebugSelectorComponent } from "../../debug"; diff --git a/packages/coding-agent/src/modes/controllers/ssh-command-controller.ts b/packages/coding-agent/src/modes/controllers/ssh-command-controller.ts index d0d31f399..59ea119c2 100644 --- a/packages/coding-agent/src/modes/controllers/ssh-command-controller.ts +++ b/packages/coding-agent/src/modes/controllers/ssh-command-controller.ts @@ -4,7 +4,7 @@ * Handles /ssh subcommands for managing SSH host configurations. */ import { Spacer, Text } from "@oh-my-pi/pi-tui"; -import { getProjectDir, getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, getSSHConfigPath } from "@oh-my-pi/pi-utils"; import { type SSHHost, sshCapability } from "../../capability/ssh"; import { loadCapability } from "../../discovery"; import { addSSHHost, readSSHConfigFile, removeSSHHost, type SSHHostConfig } from "../../ssh/config-writer"; diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index f32562d47..f1698853b 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -15,8 +15,7 @@ import { Text, TUI, } from "@oh-my-pi/pi-tui"; -import { hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils"; -import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { KeybindingsManager } from "../config/keybindings"; import { renderPromptTemplate } from "../config/prompt-templates"; @@ -776,7 +775,7 @@ export class InteractiveMode implements InteractiveModeContext { await this.sessionManager.flush(); // Emit shutdown event to hooks - await this.session.emitCustomToolSessionEvent("shutdown"); + await this.session.dispose(); if (this.isInitialized) { this.ui.requestRender(true); diff --git a/packages/coding-agent/src/modes/shared.ts b/packages/coding-agent/src/modes/shared.ts index 37ea49c4d..1698845e6 100644 --- a/packages/coding-agent/src/modes/shared.ts +++ b/packages/coding-agent/src/modes/shared.ts @@ -1,8 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { TabBarTheme } from "@oh-my-pi/pi-tui"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir, isEnoent } from "@oh-my-pi/pi-utils"; import { theme } from "./theme/theme"; // ═══════════════════════════════════════════════════════════════════════════ diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index f3bdc911f..834031e41 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -6,8 +6,7 @@ import { supportsLanguage as nativeSupportsLanguage, } from "@oh-my-pi/pi-natives"; import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui"; -import { adjustHsv, isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getCustomThemesDir } from "@oh-my-pi/pi-utils/dirs"; +import { adjustHsv, getCustomThemesDir, 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"; diff --git a/packages/coding-agent/src/patch/index.ts b/packages/coding-agent/src/patch/index.ts index 713983f9f..1056e47ba 100644 --- a/packages/coding-agent/src/patch/index.ts +++ b/packages/coding-agent/src/patch/index.ts @@ -77,31 +77,7 @@ export { normalizeCreateContent, normalizeDiff, parseHunks as parseDiffHunks } f export type { EditRenderContext, EditToolDetails } from "./shared"; // Rendering export { editToolRenderer, getLspBatchRequest } from "./shared"; -export type { - ApplyPatchOptions, - ApplyPatchResult, - ContextLineResult, - DiffError, - DiffError as EditDiffError, - DiffHunk, - DiffHunk as UpdateChunk, - DiffHunk as UpdateFileChunk, - DiffResult, - DiffResult as EditDiffResult, - FileChange, - FileSystem, - FuzzyMatch as EditMatch, - FuzzyMatch, - HashMismatch, - MatchOutcome as EditMatchOutcome, - MatchOutcome, - Operation, - PatchInput, - SequenceSearchResult, -} from "./types"; -// Types -// Legacy aliases for backwards compatibility -export { ApplyPatchError, EditMatchError, ParseError } from "./types"; +export * from "./types"; // ═══════════════════════════════════════════════════════════════════════════ // Schemas diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 37412d1db..d83439ddc 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -9,8 +9,7 @@ import { import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai"; import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; import type { Component } from "@oh-my-pi/pi-tui"; -import { $env, logger, postmortem } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath, getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getAgentDbPath, getAgentDir, getProjectDir, logger, postmortem } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { AsyncJobManager } from "./async"; import { loadCapability } from "./capability"; diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index bba63caf8..4f0b8d4ea 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -38,8 +38,7 @@ import type { UsageReport, } from "@oh-my-pi/pi-ai"; import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai"; -import { abortableSleep, isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils"; import type { AsyncJob, AsyncJobManager } from "../async"; import type { Rule } from "../capability/rule"; import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "../config/model-registry"; @@ -84,8 +83,6 @@ import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" }; import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" }; import type { SecretObfuscator } from "../secrets/obfuscator"; -import { closeAllConnections } from "../ssh/connection-manager"; -import { unmountAll } from "../ssh/sshfs-mount"; import { outputMeta } from "../tools/output-meta"; import { resolveToCwd } from "../tools/path-utils"; import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write"; @@ -273,15 +270,6 @@ const noOpUIContext: ExtensionUIContext = { setToolsExpanded: () => {}, }; -async function cleanupSshResources(): Promise { - const results = await Promise.allSettled([closeAllConnections(), unmountAll()]); - for (const result of results) { - if (result.status === "rejected") { - logger.warn("SSH cleanup failed", { error: String(result.reason) }); - } - } -} - // ============================================================================ // AgentSession Class // ============================================================================ @@ -1275,13 +1263,19 @@ export class AgentSession { * Call this when completely done with the session. */ async dispose(): Promise { + try { + if (this.#extensionRunner?.hasHandlers("session_shutdown")) { + await this.#extensionRunner.emit({ type: "session_shutdown" }); + } + } catch (error) { + logger.warn("Failed to emit session_shutdown event", { error: String(error) }); + } const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 }); const deliveryState = this.#asyncJobManager?.getDeliveryState(); if (drained === false && deliveryState) { logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState }); } await this.sessionManager.flush(); - await cleanupSshResources(); for (const state of this.#providerSessionState.values()) { state.close(); } @@ -4861,15 +4855,4 @@ Be thorough - include exact file paths, function names, error messages, and tech get extensionRunner(): ExtensionRunner | undefined { return this.#extensionRunner; } - - /** - * Emit a custom tool session event (backwards compatibility for older callers). - */ - async emitCustomToolSessionEvent(reason: "start" | "switch" | "branch" | "tree" | "shutdown"): Promise { - if (reason !== "shutdown") return; - if (this.#extensionRunner?.hasHandlers("session_shutdown")) { - await this.#extensionRunner.emit({ type: "session_shutdown" }); - } - await cleanupSshResources(); - } } diff --git a/packages/coding-agent/src/session/agent-storage.ts b/packages/coding-agent/src/session/agent-storage.ts index 11fe31c3f..ad4725481 100644 --- a/packages/coding-agent/src/session/agent-storage.ts +++ b/packages/coding-agent/src/session/agent-storage.ts @@ -2,8 +2,7 @@ import { Database, type Statement } from "bun:sqlite"; import * as fs from "node:fs"; import * as path from "node:path"; import { type AuthCredential, AuthCredentialStore, type StoredAuthCredential } from "@oh-my-pi/pi-ai"; -import { isRecord, logger } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, isRecord, logger } from "@oh-my-pi/pi-utils"; import type { RawSettings as Settings } from "../config/settings"; /** Row shape for settings table queries */ diff --git a/packages/coding-agent/src/session/history-storage.ts b/packages/coding-agent/src/session/history-storage.ts index dda9cd57b..4a4cd21a5 100644 --- a/packages/coding-agent/src/session/history-storage.ts +++ b/packages/coding-agent/src/session/history-storage.ts @@ -1,8 +1,7 @@ import { Database, type Statement } from "bun:sqlite"; import * as fs from "node:fs"; import * as path from "node:path"; -import { logger } from "@oh-my-pi/pi-utils"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir, logger } from "@oh-my-pi/pi-utils"; export interface HistoryEntry { id: number; diff --git a/packages/coding-agent/src/session/session-manager.ts b/packages/coding-agent/src/session/session-manager.ts index d3b880204..ece2fb40f 100644 --- a/packages/coding-agent/src/session/session-manager.ts +++ b/packages/coding-agent/src/session/session-manager.ts @@ -4,8 +4,16 @@ 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 { getTerminalId } from "@oh-my-pi/pi-tui"; -import { isEnoent, logger, parseJsonlLenient, Snowflake, toError } from "@oh-my-pi/pi-utils"; -import { getBlobsDir, getAgentDir as getDefaultAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { + getBlobsDir, + getAgentDir as getDefaultAgentDir, + getProjectDir, + isEnoent, + logger, + parseJsonlLenient, + Snowflake, + toError, +} from "@oh-my-pi/pi-utils"; import { ArtifactManager } from "./artifacts"; import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store"; import { diff --git a/packages/coding-agent/src/ssh/connection-manager.ts b/packages/coding-agent/src/ssh/connection-manager.ts index a9fd74978..9658cebff 100644 --- a/packages/coding-agent/src/ssh/connection-manager.ts +++ b/packages/coding-agent/src/ssh/connection-manager.ts @@ -1,7 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getRemoteHostDir, getSshControlDir } from "@oh-my-pi/pi-utils/dirs"; +import { getRemoteHostDir, getSshControlDir, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import { buildSshTarget, sanitizeHostName } from "./utils"; @@ -362,6 +361,8 @@ export async function buildRemoteCommand(host: SSHConnectionTarget, command: str return [...buildCommonArgs(host), buildSshTarget(host.username, host.host), command]; } +let registered = false; + export async function ensureConnection(host: SSHConnectionTarget): Promise { const key = host.name; const pending = pendingConnections.get(key); @@ -375,6 +376,13 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise ensureControlDir(); await validateKeyPermissions(host.keyPath); + if (!registered) { + registered = true; + postmortem.register("ssh-cleanup", async () => { + await closeAllConnections(); + }); + } + const target = buildSshTarget(host.username, host.host); const check = await runSshSync(["-O", "check", ...buildCommonArgs(host), target]); if (check.exitCode === 0) { diff --git a/packages/coding-agent/src/ssh/sshfs-mount.ts b/packages/coding-agent/src/ssh/sshfs-mount.ts index a4cbeff40..9be8900e6 100644 --- a/packages/coding-agent/src/ssh/sshfs-mount.ts +++ b/packages/coding-agent/src/ssh/sshfs-mount.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { getRemoteDir } from "@oh-my-pi/pi-utils/dirs"; +import { getRemoteDir, postmortem } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import { getControlDir, getControlPathTemplate, type SSHConnectionTarget } from "./connection-manager"; import { buildSshTarget, sanitizeHostName } from "./utils"; @@ -83,6 +83,8 @@ export async function isMounted(path: string): Promise { return result.exitCode === 0; } +let registered = false; + export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"): Promise { if (!hasSshfs()) return undefined; @@ -90,6 +92,10 @@ export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"): await Promise.all([ensureDir(REMOTE_DIR), ensureDir(CONTROL_DIR), ensureDir(mountPath)]); if (await isMounted(mountPath)) { + if (!registered) { + registered = true; + postmortem.register("sshfs-cleanup", unmountAll); + } mountedPaths.add(mountPath); return mountPath; } diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 2beaca69d..ddb717e05 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -5,8 +5,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { $env, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; import { contextFileCapability } from "./capability/context-file"; import { systemPromptCapability } from "./capability/system-prompt"; diff --git a/packages/coding-agent/src/task/worktree.ts b/packages/coding-agent/src/task/worktree.ts index 33570ffc8..f49af82f5 100644 --- a/packages/coding-agent/src/task/worktree.ts +++ b/packages/coding-agent/src/task/worktree.ts @@ -1,8 +1,7 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import path from "node:path"; -import { isEnoent, Snowflake } from "@oh-my-pi/pi-utils"; -import { getWorktreeDir } from "@oh-my-pi/pi-utils/dirs"; +import { getWorktreeDir, isEnoent, Snowflake } from "@oh-my-pi/pi-utils"; import { $ } from "bun"; export interface WorktreeBaseline { diff --git a/packages/coding-agent/src/tools/ask.ts b/packages/coding-agent/src/tools/ask.ts index 2f76ab437..e430366cc 100644 --- a/packages/coding-agent/src/tools/ask.ts +++ b/packages/coding-agent/src/tools/ask.ts @@ -59,7 +59,6 @@ export interface QuestionResult { } export interface AskToolDetails { - /** Single question mode (backwards compatible) */ question?: string; options?: string[]; multi?: boolean; diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 3b3b1b60d..7691db44f 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -3,8 +3,7 @@ import * as path from "node:path"; import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core"; import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; -import { $env, isEnoent } from "@oh-my-pi/pi-utils"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils"; import { Type } from "@sinclair/typebox"; import { renderPromptTemplate } from "../config/prompt-templates"; import { type BashResult, executeBash } from "../exec/bash-executor"; diff --git a/packages/coding-agent/src/tools/browser.ts b/packages/coding-agent/src/tools/browser.ts index 3cdbeecee..5d5b94cfd 100644 --- a/packages/coding-agent/src/tools/browser.ts +++ b/packages/coding-agent/src/tools/browser.ts @@ -3,8 +3,7 @@ import * as path from "node:path"; import { Readability } from "@mozilla/readability"; import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core"; import { StringEnum } from "@oh-my-pi/pi-ai"; -import { logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils"; -import { getPuppeteerDir } from "@oh-my-pi/pi-utils/dirs"; +import { getPuppeteerDir, logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils"; import { type Static, Type } from "@sinclair/typebox"; import { type HTMLElement, parseHTML } from "linkedom"; import type { diff --git a/packages/coding-agent/src/tools/python.ts b/packages/coding-agent/src/tools/python.ts index 4dd37b3c6..7190a02b1 100644 --- a/packages/coding-agent/src/tools/python.ts +++ b/packages/coding-agent/src/tools/python.ts @@ -4,7 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb import type { ImageContent } from "@oh-my-pi/pi-ai"; import type { Component } from "@oh-my-pi/pi-tui"; import { Markdown, Text } from "@oh-my-pi/pi-tui"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; import { type Static, Type } from "@sinclair/typebox"; import { renderPromptTemplate } from "../config/prompt-templates"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts index af54eac3d..e3c381b76 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -5,8 +5,7 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai"; import { FileType, glob } from "@oh-my-pi/pi-natives"; import type { Component } from "@oh-my-pi/pi-tui"; import { Text } from "@oh-my-pi/pi-tui"; -import { ptree, untilAborted } from "@oh-my-pi/pi-utils"; -import { getRemoteDir } from "@oh-my-pi/pi-utils/dirs"; +import { getRemoteDir, ptree, untilAborted } from "@oh-my-pi/pi-utils"; import { type Static, Type } from "@sinclair/typebox"; import { renderPromptTemplate } from "../config/prompt-templates"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index 581cbb27b..e1a7df9a2 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -1,8 +1,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { logger, ptree, TempDir } from "@oh-my-pi/pi-utils"; -import { APP_NAME, getToolsDir } from "@oh-my-pi/pi-utils/dirs"; +import { APP_NAME, getToolsDir, logger, ptree, TempDir } from "@oh-my-pi/pi-utils"; const TOOLS_DIR = getToolsDir(); const TOOL_DOWNLOAD_TIMEOUT_MS = 15000; diff --git a/packages/coding-agent/src/web/scrapers/artifacthub.ts b/packages/coding-agent/src/web/scrapers/artifacthub.ts index 1acc88c68..c90c78521 100644 --- a/packages/coding-agent/src/web/scrapers/artifacthub.ts +++ b/packages/coding-agent/src/web/scrapers/artifacthub.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; interface ArtifactHubMaintainer { name: string; diff --git a/packages/coding-agent/src/web/scrapers/aur.ts b/packages/coding-agent/src/web/scrapers/aur.ts index d8bf1e237..2a662f3fb 100644 --- a/packages/coding-agent/src/web/scrapers/aur.ts +++ b/packages/coding-agent/src/web/scrapers/aur.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; interface AurPackage { Name: string; diff --git a/packages/coding-agent/src/web/scrapers/biorxiv.ts b/packages/coding-agent/src/web/scrapers/biorxiv.ts index fe86f9e66..00c8365af 100644 --- a/packages/coding-agent/src/web/scrapers/biorxiv.ts +++ b/packages/coding-agent/src/web/scrapers/biorxiv.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface BiorxivPaper { biorxiv_doi?: string; diff --git a/packages/coding-agent/src/web/scrapers/bluesky.ts b/packages/coding-agent/src/web/scrapers/bluesky.ts index fffe5ba1c..a968b41ff 100644 --- a/packages/coding-agent/src/web/scrapers/bluesky.ts +++ b/packages/coding-agent/src/web/scrapers/bluesky.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; const API_BASE = "https://public.api.bsky.app/xrpc"; diff --git a/packages/coding-agent/src/web/scrapers/chocolatey.ts b/packages/coding-agent/src/web/scrapers/chocolatey.ts index 430c6f18a..7d848daef 100644 --- a/packages/coding-agent/src/web/scrapers/chocolatey.ts +++ b/packages/coding-agent/src/web/scrapers/chocolatey.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; interface NuGetODataEntry { Id: string; diff --git a/packages/coding-agent/src/web/scrapers/cisa-kev.ts b/packages/coding-agent/src/web/scrapers/cisa-kev.ts index 54ce1be05..6ec52b713 100644 --- a/packages/coding-agent/src/web/scrapers/cisa-kev.ts +++ b/packages/coding-agent/src/web/scrapers/cisa-kev.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface KevEntry { cveID: string; diff --git a/packages/coding-agent/src/web/scrapers/clojars.ts b/packages/coding-agent/src/web/scrapers/clojars.ts index 1d2af2ca1..0eba10245 100644 --- a/packages/coding-agent/src/web/scrapers/clojars.ts +++ b/packages/coding-agent/src/web/scrapers/clojars.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; import { asNumber, asString, isRecord } from "./utils"; function formatLicenses(licenses: unknown): string[] { diff --git a/packages/coding-agent/src/web/scrapers/coingecko.ts b/packages/coding-agent/src/web/scrapers/coingecko.ts index 1604b299d..d215787dc 100644 --- a/packages/coding-agent/src/web/scrapers/coingecko.ts +++ b/packages/coding-agent/src/web/scrapers/coingecko.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface CoinGeckoResponse { id: string; diff --git a/packages/coding-agent/src/web/scrapers/crates-io.ts b/packages/coding-agent/src/web/scrapers/crates-io.ts index c88701cc0..0c3b18906 100644 --- a/packages/coding-agent/src/web/scrapers/crates-io.ts +++ b/packages/coding-agent/src/web/scrapers/crates-io.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; /** * Check if content looks like HTML diff --git a/packages/coding-agent/src/web/scrapers/crossref.ts b/packages/coding-agent/src/web/scrapers/crossref.ts index 87e2e1185..b0e2b89cb 100644 --- a/packages/coding-agent/src/web/scrapers/crossref.ts +++ b/packages/coding-agent/src/web/scrapers/crossref.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, htmlToBasicMarkdown, loadPage } from "./types"; interface CrossrefAuthor { given?: string; diff --git a/packages/coding-agent/src/web/scrapers/discogs.ts b/packages/coding-agent/src/web/scrapers/discogs.ts index e6e812835..58d82a9b9 100644 --- a/packages/coding-agent/src/web/scrapers/discogs.ts +++ b/packages/coding-agent/src/web/scrapers/discogs.ts @@ -4,8 +4,10 @@ * Uses the Discogs API to extract structured metadata about releases. * API docs: https://www.discogs.com/developers */ + +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface DiscogsArtist { name: string; diff --git a/packages/coding-agent/src/web/scrapers/discourse.ts b/packages/coding-agent/src/web/scrapers/discourse.ts index 96bb20114..61e472b4b 100644 --- a/packages/coding-agent/src/web/scrapers/discourse.ts +++ b/packages/coding-agent/src/web/scrapers/discourse.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage } from "./types"; interface DiscourseUser { username?: string; diff --git a/packages/coding-agent/src/web/scrapers/dockerhub.ts b/packages/coding-agent/src/web/scrapers/dockerhub.ts index 55bc035af..1b6f3369b 100644 --- a/packages/coding-agent/src/web/scrapers/dockerhub.ts +++ b/packages/coding-agent/src/web/scrapers/dockerhub.ts @@ -1,6 +1,7 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { formatBytes } from "../../tools/render-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; interface DockerHubRepo { name: string; diff --git a/packages/coding-agent/src/web/scrapers/fdroid.ts b/packages/coding-agent/src/web/scrapers/fdroid.ts index 3f3271a35..8a94bd9e3 100644 --- a/packages/coding-agent/src/web/scrapers/fdroid.ts +++ b/packages/coding-agent/src/web/scrapers/fdroid.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { LocalizedText, RenderResult, SpecialHandler } from "./types"; -import { buildResult, getLocalizedText, loadPage, tryParseJson } from "./types"; +import { buildResult, getLocalizedText, loadPage } from "./types"; type FdroidPackage = { packageName?: string; diff --git a/packages/coding-agent/src/web/scrapers/firefox-addons.ts b/packages/coding-agent/src/web/scrapers/firefox-addons.ts index e4eda0caa..f3394f649 100644 --- a/packages/coding-agent/src/web/scrapers/firefox-addons.ts +++ b/packages/coding-agent/src/web/scrapers/firefox-addons.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { LocalizedText, RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, getLocalizedText, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, getLocalizedText, htmlToBasicMarkdown, loadPage } from "./types"; type AddonFile = { permissions?: string[]; diff --git a/packages/coding-agent/src/web/scrapers/flathub.ts b/packages/coding-agent/src/web/scrapers/flathub.ts index 190194a68..79ba5f667 100644 --- a/packages/coding-agent/src/web/scrapers/flathub.ts +++ b/packages/coding-agent/src/web/scrapers/flathub.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, htmlToBasicMarkdown, loadPage } from "./types"; interface FlathubScreenshotSize { src?: string; diff --git a/packages/coding-agent/src/web/scrapers/gitlab.ts b/packages/coding-agent/src/web/scrapers/gitlab.ts index 02faabe25..0e04debb7 100644 --- a/packages/coding-agent/src/web/scrapers/gitlab.ts +++ b/packages/coding-agent/src/web/scrapers/gitlab.ts @@ -1,3 +1,4 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { buildResult, formatIsoDate, @@ -6,7 +7,6 @@ import { loadPage, type RenderResult, type SpecialHandler, - tryParseJson, } from "./types"; interface GitLabUrl { diff --git a/packages/coding-agent/src/web/scrapers/go-pkg.ts b/packages/coding-agent/src/web/scrapers/go-pkg.ts index d0c85a82a..3a3db2640 100644 --- a/packages/coding-agent/src/web/scrapers/go-pkg.ts +++ b/packages/coding-agent/src/web/scrapers/go-pkg.ts @@ -1,6 +1,7 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { parse as parseHtml } from "node-html-parser"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, htmlToBasicMarkdown, loadPage } from "./types"; interface GoModuleInfo { Version: string; diff --git a/packages/coding-agent/src/web/scrapers/hackage.ts b/packages/coding-agent/src/web/scrapers/hackage.ts index f1a5c52de..997e90a69 100644 --- a/packages/coding-agent/src/web/scrapers/hackage.ts +++ b/packages/coding-agent/src/web/scrapers/hackage.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface HackageVersionMap { [version: string]: string; diff --git a/packages/coding-agent/src/web/scrapers/hackernews.ts b/packages/coding-agent/src/web/scrapers/hackernews.ts index 15a68ffeb..9884c2d1c 100644 --- a/packages/coding-agent/src/web/scrapers/hackernews.ts +++ b/packages/coding-agent/src/web/scrapers/hackernews.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, decodeHtmlEntities, formatIsoDate, loadPage, tryParseJson } from "./types"; +import { buildResult, decodeHtmlEntities, formatIsoDate, loadPage } from "./types"; interface HNItem { id: number; diff --git a/packages/coding-agent/src/web/scrapers/hex.ts b/packages/coding-agent/src/web/scrapers/hex.ts index d961c35f4..f3ba32ee6 100644 --- a/packages/coding-agent/src/web/scrapers/hex.ts +++ b/packages/coding-agent/src/web/scrapers/hex.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; /** * Handle Hex.pm (Elixir package registry) URLs via API diff --git a/packages/coding-agent/src/web/scrapers/huggingface.ts b/packages/coding-agent/src/web/scrapers/huggingface.ts index 26b2ca3ea..29d9f4975 100644 --- a/packages/coding-agent/src/web/scrapers/huggingface.ts +++ b/packages/coding-agent/src/web/scrapers/huggingface.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface HfModelData { modelId: string; diff --git a/packages/coding-agent/src/web/scrapers/jetbrains-marketplace.ts b/packages/coding-agent/src/web/scrapers/jetbrains-marketplace.ts index ad1aea79c..13aa03c5e 100644 --- a/packages/coding-agent/src/web/scrapers/jetbrains-marketplace.ts +++ b/packages/coding-agent/src/web/scrapers/jetbrains-marketplace.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage } from "./types"; interface PluginVendor { name?: string; diff --git a/packages/coding-agent/src/web/scrapers/lemmy.ts b/packages/coding-agent/src/web/scrapers/lemmy.ts index 6a50bb54e..6819c27d9 100644 --- a/packages/coding-agent/src/web/scrapers/lemmy.ts +++ b/packages/coding-agent/src/web/scrapers/lemmy.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface LemmyCreator { name: string; diff --git a/packages/coding-agent/src/web/scrapers/lobsters.ts b/packages/coding-agent/src/web/scrapers/lobsters.ts index 8449f1d2c..a58e597a1 100644 --- a/packages/coding-agent/src/web/scrapers/lobsters.ts +++ b/packages/coding-agent/src/web/scrapers/lobsters.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, loadPage } from "./types"; // ============================================================================= // Lobste.rs Types diff --git a/packages/coding-agent/src/web/scrapers/mastodon.ts b/packages/coding-agent/src/web/scrapers/mastodon.ts index 28ee824a1..b7297f1d8 100644 --- a/packages/coding-agent/src/web/scrapers/mastodon.ts +++ b/packages/coding-agent/src/web/scrapers/mastodon.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage } from "./types"; interface MastodonAccount { id: string; diff --git a/packages/coding-agent/src/web/scrapers/maven.ts b/packages/coding-agent/src/web/scrapers/maven.ts index aebdb3e50..9fb22bde9 100644 --- a/packages/coding-agent/src/web/scrapers/maven.ts +++ b/packages/coding-agent/src/web/scrapers/maven.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; interface MavenDoc { id: string; diff --git a/packages/coding-agent/src/web/scrapers/mdn.ts b/packages/coding-agent/src/web/scrapers/mdn.ts index f67ac1f8e..3371242f6 100644 --- a/packages/coding-agent/src/web/scrapers/mdn.ts +++ b/packages/coding-agent/src/web/scrapers/mdn.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, htmlToBasicMarkdown, loadPage } from "./types"; interface MDNSection { type: string; diff --git a/packages/coding-agent/src/web/scrapers/metacpan.ts b/packages/coding-agent/src/web/scrapers/metacpan.ts index 73a8c187e..dc67159a1 100644 --- a/packages/coding-agent/src/web/scrapers/metacpan.ts +++ b/packages/coding-agent/src/web/scrapers/metacpan.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, loadPage } from "./types"; interface ModuleResponse { name: string; diff --git a/packages/coding-agent/src/web/scrapers/musicbrainz.ts b/packages/coding-agent/src/web/scrapers/musicbrainz.ts index 8220d03e9..4d6537e4b 100644 --- a/packages/coding-agent/src/web/scrapers/musicbrainz.ts +++ b/packages/coding-agent/src/web/scrapers/musicbrainz.ts @@ -1,8 +1,10 @@ /** * MusicBrainz URL handler for artists, releases, and recordings */ + +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatMediaDuration, loadPage, tryParseJson } from "./types"; +import { buildResult, formatMediaDuration, loadPage } from "./types"; type MusicBrainzEntity = "artist" | "release" | "recording"; diff --git a/packages/coding-agent/src/web/scrapers/npm.ts b/packages/coding-agent/src/web/scrapers/npm.ts index 09cb393db..84fc06c99 100644 --- a/packages/coding-agent/src/web/scrapers/npm.ts +++ b/packages/coding-agent/src/web/scrapers/npm.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; /** * Handle npm URLs via registry API diff --git a/packages/coding-agent/src/web/scrapers/nuget.ts b/packages/coding-agent/src/web/scrapers/nuget.ts index e35a5c75f..0afbd768f 100644 --- a/packages/coding-agent/src/web/scrapers/nuget.ts +++ b/packages/coding-agent/src/web/scrapers/nuget.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types"; interface NuGetCatalogEntry { id: string; diff --git a/packages/coding-agent/src/web/scrapers/nvd.ts b/packages/coding-agent/src/web/scrapers/nvd.ts index e24ce65d0..20392d877 100644 --- a/packages/coding-agent/src/web/scrapers/nvd.ts +++ b/packages/coding-agent/src/web/scrapers/nvd.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, loadPage } from "./types"; interface CvssV31 { baseScore: number; diff --git a/packages/coding-agent/src/web/scrapers/ollama.ts b/packages/coding-agent/src/web/scrapers/ollama.ts index 5be0b2d98..91ee2613a 100644 --- a/packages/coding-agent/src/web/scrapers/ollama.ts +++ b/packages/coding-agent/src/web/scrapers/ollama.ts @@ -1,6 +1,7 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import { formatBytes } from "../../tools/render-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, decodeHtmlEntities, loadPage, tryParseJson } from "./types"; +import { buildResult, decodeHtmlEntities, loadPage } from "./types"; interface OllamaTagDetails { parent_model?: string; diff --git a/packages/coding-agent/src/web/scrapers/open-vsx.ts b/packages/coding-agent/src/web/scrapers/open-vsx.ts index 1e5f072b9..2ecd8a294 100644 --- a/packages/coding-agent/src/web/scrapers/open-vsx.ts +++ b/packages/coding-agent/src/web/scrapers/open-vsx.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface OpenVsxFileLinks { readme?: string; diff --git a/packages/coding-agent/src/web/scrapers/opencorporates.ts b/packages/coding-agent/src/web/scrapers/opencorporates.ts index 6370b554a..74e2b7db1 100644 --- a/packages/coding-agent/src/web/scrapers/opencorporates.ts +++ b/packages/coding-agent/src/web/scrapers/opencorporates.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface Officer { id: number; diff --git a/packages/coding-agent/src/web/scrapers/openlibrary.ts b/packages/coding-agent/src/web/scrapers/openlibrary.ts index 91ef9cbe6..24ff229b5 100644 --- a/packages/coding-agent/src/web/scrapers/openlibrary.ts +++ b/packages/coding-agent/src/web/scrapers/openlibrary.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface OpenLibraryAuthor { name?: string; diff --git a/packages/coding-agent/src/web/scrapers/orcid.ts b/packages/coding-agent/src/web/scrapers/orcid.ts index 78bb0394d..90c9ad933 100644 --- a/packages/coding-agent/src/web/scrapers/orcid.ts +++ b/packages/coding-agent/src/web/scrapers/orcid.ts @@ -1,8 +1,10 @@ /** * ORCID handler for web-fetch */ + +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; const MAX_WORKS = 50; const ORCID_PATTERN = /\/(\d{4}-\d{4}-\d{4}-\d{3}[\dXx])(?:\/|$)/; diff --git a/packages/coding-agent/src/web/scrapers/osv.ts b/packages/coding-agent/src/web/scrapers/osv.ts index 0e49f49ef..a5f033c5d 100644 --- a/packages/coding-agent/src/web/scrapers/osv.ts +++ b/packages/coding-agent/src/web/scrapers/osv.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, loadPage } from "./types"; interface OsvSeverity { type: string; diff --git a/packages/coding-agent/src/web/scrapers/packagist.ts b/packages/coding-agent/src/web/scrapers/packagist.ts index 184ba4fa7..e5237ec5c 100644 --- a/packages/coding-agent/src/web/scrapers/packagist.ts +++ b/packages/coding-agent/src/web/scrapers/packagist.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; /** * Handle Packagist URLs via JSON API diff --git a/packages/coding-agent/src/web/scrapers/pub-dev.ts b/packages/coding-agent/src/web/scrapers/pub-dev.ts index 4a43ad5ef..1bf9f02b7 100644 --- a/packages/coding-agent/src/web/scrapers/pub-dev.ts +++ b/packages/coding-agent/src/web/scrapers/pub-dev.ts @@ -1,4 +1,5 @@ -import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage, type SpecialHandler } from "./types"; /** * Handle pub.dev URLs via API diff --git a/packages/coding-agent/src/web/scrapers/pubmed.ts b/packages/coding-agent/src/web/scrapers/pubmed.ts index 960e0f996..ebc7fdf4f 100644 --- a/packages/coding-agent/src/web/scrapers/pubmed.ts +++ b/packages/coding-agent/src/web/scrapers/pubmed.ts @@ -1,7 +1,8 @@ /** * PubMed handler for web-fetch */ -import { buildResult, loadPage, type RenderResult, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, loadPage, type RenderResult, type SpecialHandler } from "./types"; const NCBI_HEADERS = { Accept: "application/json, text/plain;q=0.9, */*;q=0.8", diff --git a/packages/coding-agent/src/web/scrapers/pypi.ts b/packages/coding-agent/src/web/scrapers/pypi.ts index 276aeff53..1fe6e4336 100644 --- a/packages/coding-agent/src/web/scrapers/pypi.ts +++ b/packages/coding-agent/src/web/scrapers/pypi.ts @@ -1,4 +1,5 @@ -import { buildResult, formatNumber, loadPage, type RenderResult, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, formatNumber, loadPage, type RenderResult, type SpecialHandler } from "./types"; /** * Handle PyPI URLs via JSON API diff --git a/packages/coding-agent/src/web/scrapers/rawg.ts b/packages/coding-agent/src/web/scrapers/rawg.ts index 3ab157cf6..f44eb54f8 100644 --- a/packages/coding-agent/src/web/scrapers/rawg.ts +++ b/packages/coding-agent/src/web/scrapers/rawg.ts @@ -1,11 +1,5 @@ -import { - buildResult, - htmlToBasicMarkdown, - loadPage, - type RenderResult, - type SpecialHandler, - tryParseJson, -} from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, htmlToBasicMarkdown, loadPage, type RenderResult, type SpecialHandler } from "./types"; interface RawgPlatformEntry { platform?: { diff --git a/packages/coding-agent/src/web/scrapers/reddit.ts b/packages/coding-agent/src/web/scrapers/reddit.ts index 79454af15..6fd513423 100644 --- a/packages/coding-agent/src/web/scrapers/reddit.ts +++ b/packages/coding-agent/src/web/scrapers/reddit.ts @@ -1,4 +1,5 @@ -import { buildResult, formatIsoDate, loadPage, type RenderResult, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, formatIsoDate, loadPage, type RenderResult, type SpecialHandler } from "./types"; interface RedditPost { title: string; diff --git a/packages/coding-agent/src/web/scrapers/repology.ts b/packages/coding-agent/src/web/scrapers/repology.ts index 955e8e386..6ec686e72 100644 --- a/packages/coding-agent/src/web/scrapers/repology.ts +++ b/packages/coding-agent/src/web/scrapers/repology.ts @@ -1,4 +1,5 @@ -import { buildResult, loadPage, type RenderResult, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, loadPage, type RenderResult, type SpecialHandler } from "./types"; interface RepologyPackage { repo: string; diff --git a/packages/coding-agent/src/web/scrapers/rfc.ts b/packages/coding-agent/src/web/scrapers/rfc.ts index ed1187037..bfca7b9e2 100644 --- a/packages/coding-agent/src/web/scrapers/rfc.ts +++ b/packages/coding-agent/src/web/scrapers/rfc.ts @@ -1,4 +1,5 @@ -import { buildResult, loadPage, type RenderResult, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, loadPage, type RenderResult, type SpecialHandler } from "./types"; interface RfcMetadata { doc_id: string; diff --git a/packages/coding-agent/src/web/scrapers/rubygems.ts b/packages/coding-agent/src/web/scrapers/rubygems.ts index d160ee2a0..876d3ca3b 100644 --- a/packages/coding-agent/src/web/scrapers/rubygems.ts +++ b/packages/coding-agent/src/web/scrapers/rubygems.ts @@ -1,4 +1,5 @@ -import { buildResult, formatNumber, loadPage, type RenderResult, type SpecialHandler, tryParseJson } from "./types"; +import { tryParseJson } from "@oh-my-pi/pi-utils"; +import { buildResult, formatNumber, loadPage, type RenderResult, type SpecialHandler } from "./types"; interface RubyGemsDependency { name: string; diff --git a/packages/coding-agent/src/web/scrapers/searchcode.ts b/packages/coding-agent/src/web/scrapers/searchcode.ts index 30ceb72c4..3ac33fd27 100644 --- a/packages/coding-agent/src/web/scrapers/searchcode.ts +++ b/packages/coding-agent/src/web/scrapers/searchcode.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface SearchcodeResult { id?: number | string; diff --git a/packages/coding-agent/src/web/scrapers/sec-edgar.ts b/packages/coding-agent/src/web/scrapers/sec-edgar.ts index 2e654e590..03f49ea7a 100644 --- a/packages/coding-agent/src/web/scrapers/sec-edgar.ts +++ b/packages/coding-agent/src/web/scrapers/sec-edgar.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; interface SecFiling { accessionNumber: string; diff --git a/packages/coding-agent/src/web/scrapers/semantic-scholar.ts b/packages/coding-agent/src/web/scrapers/semantic-scholar.ts index e3d073444..946c822d7 100644 --- a/packages/coding-agent/src/web/scrapers/semantic-scholar.ts +++ b/packages/coding-agent/src/web/scrapers/semantic-scholar.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface SemanticScholarAuthor { name: string; diff --git a/packages/coding-agent/src/web/scrapers/snapcraft.ts b/packages/coding-agent/src/web/scrapers/snapcraft.ts index 5c65de88f..847de0d9a 100644 --- a/packages/coding-agent/src/web/scrapers/snapcraft.ts +++ b/packages/coding-agent/src/web/scrapers/snapcraft.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface SnapcraftPublisher { "display-name"?: string; diff --git a/packages/coding-agent/src/web/scrapers/sourcegraph.ts b/packages/coding-agent/src/web/scrapers/sourcegraph.ts index ea7290bce..cbf59cdb4 100644 --- a/packages/coding-agent/src/web/scrapers/sourcegraph.ts +++ b/packages/coding-agent/src/web/scrapers/sourcegraph.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, loadPage, tryParseJson } from "./types"; +import { buildResult, loadPage } from "./types"; const GRAPHQL_ENDPOINT = "https://sourcegraph.com/.api/graphql"; const GRAPHQL_HEADERS = { diff --git a/packages/coding-agent/src/web/scrapers/spdx.ts b/packages/coding-agent/src/web/scrapers/spdx.ts index 193b50a08..013f4f0f7 100644 --- a/packages/coding-agent/src/web/scrapers/spdx.ts +++ b/packages/coding-agent/src/web/scrapers/spdx.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, htmlToBasicMarkdown, loadPage } from "./types"; interface SpdxCrossRef { url?: string; diff --git a/packages/coding-agent/src/web/scrapers/stackoverflow.ts b/packages/coding-agent/src/web/scrapers/stackoverflow.ts index 8f0cc1a47..4334f2eef 100644 --- a/packages/coding-agent/src/web/scrapers/stackoverflow.ts +++ b/packages/coding-agent/src/web/scrapers/stackoverflow.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage } from "./types"; interface SOQuestion { title: string; diff --git a/packages/coding-agent/src/web/scrapers/terraform.ts b/packages/coding-agent/src/web/scrapers/terraform.ts index f17f1b626..fb7f05204 100644 --- a/packages/coding-agent/src/web/scrapers/terraform.ts +++ b/packages/coding-agent/src/web/scrapers/terraform.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface TerraformModule { id: string; diff --git a/packages/coding-agent/src/web/scrapers/types.ts b/packages/coding-agent/src/web/scrapers/types.ts index 215eaa68d..9181c9880 100644 --- a/packages/coding-agent/src/web/scrapers/types.ts +++ b/packages/coding-agent/src/web/scrapers/types.ts @@ -190,17 +190,6 @@ export function htmlToBasicMarkdown(html: string): string { return decodeHtmlEntities(stripped); } -/** - * Try to parse JSON, returning null on failure. - */ -export function tryParseJson(content: string): T | null { - try { - return JSON.parse(content) as T; - } catch { - return null; - } -} - /** * Build a RenderResult from markdown content. Calls finalizeOutput internally. */ diff --git a/packages/coding-agent/src/web/scrapers/vimeo.ts b/packages/coding-agent/src/web/scrapers/vimeo.ts index a0861284e..812106e19 100644 --- a/packages/coding-agent/src/web/scrapers/vimeo.ts +++ b/packages/coding-agent/src/web/scrapers/vimeo.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { SpecialHandler } from "./types"; -import { buildResult, formatMediaDuration, loadPage, tryParseJson } from "./types"; +import { buildResult, formatMediaDuration, loadPage } from "./types"; interface VimeoOEmbed { title: string; diff --git a/packages/coding-agent/src/web/scrapers/vscode-marketplace.ts b/packages/coding-agent/src/web/scrapers/vscode-marketplace.ts index e1c056c3d..63759ef90 100644 --- a/packages/coding-agent/src/web/scrapers/vscode-marketplace.ts +++ b/packages/coding-agent/src/web/scrapers/vscode-marketplace.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; interface MarketplaceProperty { key?: string; diff --git a/packages/coding-agent/src/web/scrapers/w3c.ts b/packages/coding-agent/src/web/scrapers/w3c.ts index fc9e7f58e..d884dc5ca 100644 --- a/packages/coding-agent/src/web/scrapers/w3c.ts +++ b/packages/coding-agent/src/web/scrapers/w3c.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types"; +import { buildResult, htmlToBasicMarkdown, loadPage } from "./types"; import { asRecord } from "./utils"; type JsonRecord = Record; diff --git a/packages/coding-agent/src/web/scrapers/wikidata.ts b/packages/coding-agent/src/web/scrapers/wikidata.ts index e656ffec4..141c676ce 100644 --- a/packages/coding-agent/src/web/scrapers/wikidata.ts +++ b/packages/coding-agent/src/web/scrapers/wikidata.ts @@ -1,5 +1,6 @@ +import { tryParseJson } from "@oh-my-pi/pi-utils"; import type { RenderResult, SpecialHandler } from "./types"; -import { buildResult, formatNumber, loadPage, tryParseJson } from "./types"; +import { buildResult, formatNumber, loadPage } from "./types"; /** * Common Wikidata property IDs mapped to human-readable names diff --git a/packages/coding-agent/src/web/search/providers/codex.ts b/packages/coding-agent/src/web/search/providers/codex.ts index 72b5faee0..eb762daaf 100644 --- a/packages/coding-agent/src/web/search/providers/codex.ts +++ b/packages/coding-agent/src/web/search/providers/codex.ts @@ -6,8 +6,7 @@ * Returns synthesized answers with web search sources. */ import * as os from "node:os"; -import { readSseJson } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, readSseJson } from "@oh-my-pi/pi-utils"; import packageJson from "../../../../package.json" with { type: "json" }; import { AgentStorage } from "../../../session/agent-storage"; import type { SearchResponse, SearchSource } from "../../../web/search/types"; diff --git a/packages/coding-agent/src/web/search/providers/gemini.ts b/packages/coding-agent/src/web/search/providers/gemini.ts index 158ae6459..1dac642a7 100644 --- a/packages/coding-agent/src/web/search/providers/gemini.ts +++ b/packages/coding-agent/src/web/search/providers/gemini.ts @@ -6,7 +6,7 @@ * Returns synthesized answers with citations and source metadata from grounding chunks. */ import { getAntigravityHeaders, getGeminiCliHeaders, refreshGoogleCloudToken } from "@oh-my-pi/pi-ai"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath } from "@oh-my-pi/pi-utils"; import { AgentStorage } from "../../../session/agent-storage"; import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types"; import { SearchProviderError } from "../../../web/search/types"; diff --git a/packages/coding-agent/src/web/search/providers/perplexity.ts b/packages/coding-agent/src/web/search/providers/perplexity.ts index 992cce7a1..46898d11d 100644 --- a/packages/coding-agent/src/web/search/providers/perplexity.ts +++ b/packages/coding-agent/src/web/search/providers/perplexity.ts @@ -7,8 +7,7 @@ */ import { getEnvApiKey } from "@oh-my-pi/pi-ai"; -import { readSseJson } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, readSseJson } from "@oh-my-pi/pi-utils"; import { AgentStorage } from "../../../session/agent-storage"; import type { PerplexityMessageOutput, diff --git a/packages/coding-agent/src/web/search/providers/utils.ts b/packages/coding-agent/src/web/search/providers/utils.ts index 39a1c136c..6086fddae 100644 --- a/packages/coding-agent/src/web/search/providers/utils.ts +++ b/packages/coding-agent/src/web/search/providers/utils.ts @@ -1,4 +1,4 @@ -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath } from "@oh-my-pi/pi-utils"; import { AgentStorage } from "../../../session/agent-storage"; /** diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 226da3abd..c2853094d 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -10,8 +10,7 @@ import { ExtensionRunner } from "@oh-my-pi/pi-coding-agent/extensibility/extensi import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -import { TempDir, withTimeout } from "@oh-my-pi/pi-utils"; -import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectAgentDir, TempDir, withTimeout } from "@oh-my-pi/pi-utils"; const runtimeSignalStoreKey = "__ompRuntimeSignals"; diff --git a/packages/coding-agent/test/autocomplete-max-visible.test.ts b/packages/coding-agent/test/autocomplete-max-visible.test.ts index 7e7099287..4e03f0614 100644 --- a/packages/coding-agent/test/autocomplete-max-visible.test.ts +++ b/packages/coding-agent/test/autocomplete-max-visible.test.ts @@ -5,8 +5,7 @@ import * as path from "node:path"; import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { getDefault } from "@oh-my-pi/pi-coding-agent/config/settings-schema"; import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/selector-controller"; -import { Snowflake } from "@oh-my-pi/pi-utils"; -import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectAgentDir, Snowflake } from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; describe("autocompleteMaxVisible setting", () => { diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index 43c6ed48e..2c0c8a4c7 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -30,7 +30,7 @@ import { e2eApiKey } from "./utilities"; // ============================================================================ async function loadLargeSessionEntries(): Promise { - const sessionPath = path.join(__dirname, "fixtures/large-session.jsonl"); + const sessionPath = path.join(import.meta.dirname, "fixtures/large-session.jsonl"); const content = await Bun.file(sessionPath).text(); const entries = parseSessionEntries(content); migrateSessionEntries(entries); // Add id/parentId for v1 fixtures diff --git a/packages/coding-agent/test/core/python-executor-lifecycle.test.ts b/packages/coding-agent/test/core/python-executor-lifecycle.test.ts index 3dc659c41..ff3acbd7f 100644 --- a/packages/coding-agent/test/core/python-executor-lifecycle.test.ts +++ b/packages/coding-agent/test/core/python-executor-lifecycle.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "bun:test"; import { disposeAllKernelSessions, executePython } from "@oh-my-pi/pi-coding-agent/ipy/executor"; import type { KernelExecuteResult } from "@oh-my-pi/pi-coding-agent/ipy/kernel"; import * as pythonKernel from "@oh-my-pi/pi-coding-agent/ipy/kernel"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; class FakeKernel { execute = vi.fn(async () => this.result); diff --git a/packages/coding-agent/test/core/python-modules.test.ts b/packages/coding-agent/test/core/python-modules.test.ts index ba0c5e0ef..727ca25dd 100644 --- a/packages/coding-agent/test/core/python-modules.test.ts +++ b/packages/coding-agent/test/core/python-modules.test.ts @@ -2,8 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import { loadPythonModules, type PythonModuleExecutor } from "@oh-my-pi/pi-coding-agent/ipy/modules"; -import { TempDir } from "@oh-my-pi/pi-utils"; -import { getAgentModulesDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentModulesDir, getProjectModulesDir, TempDir } from "@oh-my-pi/pi-utils"; const fixturesDir = path.resolve(import.meta.dir, "../../test/fixtures/python-modules"); diff --git a/packages/coding-agent/test/core/python-prelude.test.ts b/packages/coding-agent/test/core/python-prelude.test.ts index 4a5dafa27..318d6cf3e 100644 --- a/packages/coding-agent/test/core/python-prelude.test.ts +++ b/packages/coding-agent/test/core/python-prelude.test.ts @@ -4,7 +4,7 @@ import * as path from "node:path"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { resetPreludeDocsCache, warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/ipy/executor"; import { getPythonToolDescription, PythonTool } from "@oh-my-pi/pi-coding-agent/tools/python"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; const resolvePythonPath = (): string | null => { const venvPath = Bun.env.VIRTUAL_ENV; diff --git a/packages/coding-agent/test/extensions-discovery.test.ts b/packages/coding-agent/test/extensions-discovery.test.ts index 8196f9b2f..bf4265466 100644 --- a/packages/coding-agent/test/extensions-discovery.test.ts +++ b/packages/coding-agent/test/extensions-discovery.test.ts @@ -1,14 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs"; import * as path from "node:path"; -import { fileURLToPath } from "node:url"; import { discoverAndLoadExtensions, loadExtensions } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/loader"; -import { TempDir } from "@oh-my-pi/pi-utils"; -import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectAgentDir, TempDir } from "@oh-my-pi/pi-utils"; import { filterUserExtensionErrors, filterUserExtensions } from "./utils/filter-user-extensions"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - describe("extensions discovery", () => { let tempDir: TempDir; let extensionsDir: string; @@ -309,7 +305,7 @@ describe("extensions discovery", () => { it("resolves 3rd party npm dependencies (chalk)", async () => { // Load the real chalk-logger extension from examples - const chalkLoggerPath = path.resolve(__dirname, "../examples/extensions/chalk-logger.ts"); + const chalkLoggerPath = path.resolve(import.meta.dirname, "../examples/extensions/chalk-logger.ts"); const result = await discoverForTest([chalkLoggerPath]); @@ -322,7 +318,7 @@ describe("extensions discovery", () => { it("resolves dependencies from extension's own node_modules", async () => { // Load extension that has its own package.json and node_modules with 'ms' package - const extPath = path.resolve(__dirname, "../examples/extensions/with-deps"); + const extPath = path.resolve(import.meta.dirname, "../examples/extensions/with-deps"); const result = await discoverForTest([extPath]); diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 8295287a6..27ae516de 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -10,8 +10,7 @@ import { discoverAndLoadExtensions } from "@oh-my-pi/pi-coding-agent/extensibili import { ExtensionRunner } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/runner"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -import { logger, TempDir } from "@oh-my-pi/pi-utils"; -import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectAgentDir, logger, TempDir } from "@oh-my-pi/pi-utils"; import { filterUserExtensionErrors, filterUserExtensions } from "./utils/filter-user-extensions"; describe("ExtensionRunner", () => { diff --git a/packages/coding-agent/test/memories-runtime.test.ts b/packages/coding-agent/test/memories-runtime.test.ts index a2312c1e7..72b417a88 100644 --- a/packages/coding-agent/test/memories-runtime.test.ts +++ b/packages/coding-agent/test/memories-runtime.test.ts @@ -7,8 +7,7 @@ import * as ai from "@oh-my-pi/pi-ai"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { buildMemoryToolDeveloperInstructions, startMemoryStartupTask } from "@oh-my-pi/pi-coding-agent/memories"; import * as memoryStorage from "@oh-my-pi/pi-coding-agent/memories/storage"; -import { Snowflake } from "@oh-my-pi/pi-utils"; -import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDbPath, Snowflake } from "@oh-my-pi/pi-utils"; interface SessionFixture { agentDir: string; diff --git a/packages/coding-agent/test/sdk-skills.test.ts b/packages/coding-agent/test/sdk-skills.test.ts index 22c46c4fd..a9ced71d2 100644 --- a/packages/coding-agent/test/sdk-skills.test.ts +++ b/packages/coding-agent/test/sdk-skills.test.ts @@ -2,20 +2,38 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import type { Skill } from "@oh-my-pi/pi-coding-agent/sdk"; import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs"; + +function createIsolatedSkillsSettings(): Settings { + return Settings.isolated({ + "skills.enabled": true, + "skills.enableCodexUser": false, + "skills.enableClaudeUser": false, + "skills.enableClaudeProject": false, + "skills.enablePiUser": false, + "skills.enablePiProject": true, + }); +} describe("createAgentSession skills option", () => { let tempDir: string; let skillsDir: string; + let tempHomeDir = ""; + let originalHome: string | undefined; beforeEach(() => { tempDir = path.join(os.tmpdir(), `pi-sdk-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); // Create skill in .omp/skills/ for native project-level discovery - skillsDir = path.join(getProjectAgentDir(tempDir), "skills", "test-skill"); + skillsDir = path.join(tempDir, ".omp", "skills", "test-skill"); fs.mkdirSync(skillsDir, { recursive: true }); + originalHome = process.env.HOME; + tempHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-sdk-home-")); + process.env.HOME = tempHomeDir; + const nativeUserSkillsDir = path.join(tempHomeDir, ".omp", "agent", "skills"); + fs.mkdirSync(nativeUserSkillsDir, { recursive: true }); // Create a test skill in the pi skills directory fs.writeFileSync( @@ -52,6 +70,14 @@ Loaded via symbolic link. if (tempDir) { fs.rmSync(tempDir, { recursive: true, force: true }); } + if (tempHomeDir) { + fs.rmSync(tempHomeDir, { recursive: true, force: true }); + } + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } }); it("should discover skills by default and expose them on session.skills", async () => { @@ -59,6 +85,7 @@ Loaded via symbolic link. cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), + settings: createIsolatedSkillsSettings(), }); // Skills should be discovered and exposed on the session @@ -71,6 +98,7 @@ Loaded via symbolic link. cwd: tempDir, agentDir: tempDir, sessionManager: SessionManager.inMemory(), + settings: createIsolatedSkillsSettings(), }); expect(session.skills.some((s: Skill) => s.name === "symlinked-skill")).toBe(true); @@ -82,6 +110,7 @@ Loaded via symbolic link. agentDir: tempDir, sessionManager: SessionManager.inMemory(), skills: [], // Explicitly empty - like --no-skills + settings: createIsolatedSkillsSettings(), }); // session.skills should be empty @@ -104,6 +133,7 @@ Loaded via symbolic link. agentDir: tempDir, sessionManager: SessionManager.inMemory(), skills: [customSkill], + settings: createIsolatedSkillsSettings(), }); // session.skills should contain only the provided skill diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index b6c52721d..0c7bafa74 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -3,8 +3,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; -import { Snowflake } from "@oh-my-pi/pi-utils"; -import { getProjectAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectAgentDir, Snowflake } from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; describe("Settings", () => { diff --git a/packages/coding-agent/test/skills.test.ts b/packages/coding-agent/test/skills.test.ts index 82d51f5c1..f8f45a084 100644 --- a/packages/coding-agent/test/skills.test.ts +++ b/packages/coding-agent/test/skills.test.ts @@ -1,133 +1,114 @@ import { describe, expect, it } from "bun:test"; +import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { loadSkills, loadSkillsFromDir, type Skill } from "@oh-my-pi/pi-coding-agent/extensibility/skills"; -const fixturesDir = path.resolve(__dirname, "fixtures/skills"); -const collisionFixturesDir = path.resolve(__dirname, "fixtures/skills-collision"); +const fixturesDir = path.resolve(import.meta.dirname, "fixtures/skills"); +const collisionFixturesDir = path.resolve(import.meta.dirname, "fixtures/skills-collision"); describe("skills", () => { describe("loadSkillsFromDir", () => { - it("should load a valid skill", async () => { - const { skills, warnings } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "valid-skill"), - source: "test", - }); + const loadFixtureRoot = () => loadSkillsFromDir({ dir: fixturesDir, source: "test" }); - expect(skills).toHaveLength(1); - expect(skills[0].name).toBe("valid-skill"); - expect(skills[0].description).toBe("A valid skill for testing purposes."); - expect(skills[0].source).toBe("test"); + it("should load a valid skill from a skills root", async () => { + const { skills, warnings } = await loadFixtureRoot(); + const validSkill = skills.find(skill => skill.name === "valid-skill"); + + expect(validSkill).toBeDefined(); + expect(validSkill?.description).toBe("A valid skill for testing purposes."); + expect(validSkill?.source).toBe("test"); expect(warnings).toHaveLength(0); }); it("should load skill when name doesn't match parent directory", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "name-mismatch"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(1); - expect(skills[0].name).toBe("different-name"); + expect(skills.some(skill => skill.name === "different-name")).toBe(true); }); it("should load skill with invalid name characters", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "invalid-name-chars"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(1); + expect(skills.some(skill => skill.name === "Invalid_Name")).toBe(true); }); it("should load skill when name exceeds 64 characters", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "long-name"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(1); + expect( + skills.some( + skill => + skill.name === + "this-is-a-very-long-skill-name-that-exceeds-the-sixty-four-character-limit-set-by-the-standard", + ), + ).toBe(true); }); it("should skip skill when description is missing", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "missing-description"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(0); + expect(skills.some(skill => skill.name === "missing-description")).toBe(false); }); it("should load skill with unknown frontmatter fields", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "unknown-field"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(1); + expect(skills.some(skill => skill.name === "unknown-field")).toBe(true); }); - it("should load nested skills recursively", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "nested"), - source: "test", - }); + it("should not load nested skills recursively", async () => { + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(1); - expect(skills[0].name).toBe("child-skill"); + expect(skills.some(skill => skill.name === "child-skill")).toBe(false); }); it("should skip files without frontmatter description", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "no-frontmatter"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - // no-frontmatter has no description, so it should be skipped - expect(skills).toHaveLength(0); + expect(skills.some(skill => skill.name === "no-frontmatter")).toBe(false); }); it("should load skill with consecutive hyphens in name", async () => { - const { skills } = await loadSkillsFromDir({ - dir: path.join(fixturesDir, "consecutive-hyphens"), - source: "test", - }); + const { skills } = await loadFixtureRoot(); - expect(skills).toHaveLength(1); + expect(skills.some(skill => skill.name === "bad--name")).toBe(true); }); - it("should load all skills from fixture directory", async () => { - const { skills } = await loadSkillsFromDir({ - dir: fixturesDir, - source: "test", - }); + it("should load all directly nested skills from fixture directory", async () => { + const { skills } = await loadFixtureRoot(); + const names = skills.map(skill => skill.name); - // Should load all skills that have descriptions (even with warnings) - // valid-skill, name-mismatch, invalid-name-chars, long-name, unknown-field, nested/child-skill, consecutive-hyphens - // NOT: missing-description, no-frontmatter (both missing descriptions) - expect(skills.length).toBeGreaterThanOrEqual(6); + expect(names).toEqual( + expect.arrayContaining([ + "valid-skill", + "different-name", + "Invalid_Name", + "this-is-a-very-long-skill-name-that-exceeds-the-sixty-four-character-limit-set-by-the-standard", + "unknown-field", + "bad--name", + ]), + ); + expect(names).not.toContain("child-skill"); + expect(skills).toHaveLength(6); }); - it("should return empty for non-existent directory", async () => { - const { skills } = await loadSkillsFromDir({ - dir: "/non/existent/path", - source: "test", - }); - - expect(skills).toHaveLength(0); + it("should throw for non-existent directory", () => { + expect( + loadSkillsFromDir({ + dir: "/non/existent/path", + source: "test", + }), + ).rejects.toThrow("ENOENT"); }); - it("should use parent directory name when name not in frontmatter", async () => { - // The no-frontmatter fixture has no name in frontmatter, so it should use "no-frontmatter" - // But it also has no description, so it won't load - // Let's test with a valid skill that relies on directory name + it("should return empty when scanning a single skill directory directly", async () => { const { skills } = await loadSkillsFromDir({ dir: path.join(fixturesDir, "valid-skill"), source: "test", }); - expect(skills).toHaveLength(1); - expect(skills[0].name).toBe("valid-skill"); + expect(skills).toHaveLength(0); }); }); @@ -153,10 +134,10 @@ describe("skills", () => { enableClaudeProject: false, enablePiUser: false, enablePiProject: false, - customDirectories: [path.join(fixturesDir, "valid-skill")], + customDirectories: [fixturesDir], ignoredSkills: ["valid-skill"], }); - expect(skills).toHaveLength(0); + expect(skills.some(s => s.name === "valid-skill")).toBe(false); }); it("should support glob patterns in ignoredSkills", async () => { @@ -188,24 +169,45 @@ describe("skills", () => { }); it("should expand ~ in customDirectories", async () => { - const homeSkillsDir = path.join(os.homedir(), ".omp/agent/skills"); - const { skills: withTilde } = await loadSkills({ - enableCodexUser: false, - enableClaudeUser: false, - enableClaudeProject: false, - enablePiUser: false, - enablePiProject: false, - customDirectories: ["~/.omp/agent/skills"], - }); - const { skills: withoutTilde } = await loadSkills({ - enableCodexUser: false, - enableClaudeUser: false, - enableClaudeProject: false, - enablePiUser: false, - enablePiProject: false, - customDirectories: [homeSkillsDir], - }); - expect(withTilde.length).toBe(withoutTilde.length); + const tempHomeSkillsDir = await fs.mkdtemp(path.join(os.homedir(), ".pi-skills-test-")); + const relativeToHome = path.relative(os.homedir(), tempHomeSkillsDir); + const tildeDir = `~/${relativeToHome.split(path.sep).join("/")}`; + const skillDir = path.join(tempHomeSkillsDir, "tilde-skill"); + const skillPath = path.join(skillDir, "SKILL.md"); + await fs.mkdir(skillDir, { recursive: true }); + await fs.writeFile( + skillPath, + `--- +name: tilde-skill +description: Skill loaded from a tilde-expanded custom directory. +--- + +# Tilde Skill +`, + ); + + try { + const { skills: withTilde } = await loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [tildeDir], + }); + const { skills: withoutTilde } = await loadSkills({ + enableCodexUser: false, + enableClaudeUser: false, + enableClaudeProject: false, + enablePiUser: false, + enablePiProject: false, + customDirectories: [tempHomeSkillsDir], + }); + expect(withTilde.length).toBe(withoutTilde.length); + expect(withTilde.some(skill => skill.name === "tilde-skill")).toBe(true); + } finally { + await fs.rm(tempHomeSkillsDir, { recursive: true, force: true }); + } }); it("should return empty when all sources disabled and no custom dirs", async () => { diff --git a/packages/coding-agent/test/utils/filter-user-extensions.ts b/packages/coding-agent/test/utils/filter-user-extensions.ts index fbe02661e..2a386108b 100644 --- a/packages/coding-agent/test/utils/filter-user-extensions.ts +++ b/packages/coding-agent/test/utils/filter-user-extensions.ts @@ -1,5 +1,5 @@ import * as path from "node:path"; -import { getAgentDir } from "@oh-my-pi/pi-utils/dirs"; +import { getAgentDir } from "@oh-my-pi/pi-utils"; export function filterUserExtensions(extensions: T[]): T[] { const userExtensionsDir = path.join(getAgentDir(), "extensions"); diff --git a/packages/natives/src/native.ts b/packages/natives/src/native.ts index 9053480a7..60c22e260 100644 --- a/packages/natives/src/native.ts +++ b/packages/natives/src/native.ts @@ -7,8 +7,7 @@ import * as fs from "node:fs"; import { createRequire } from "node:module"; import * as os from "node:os"; import * as path from "node:path"; -import { $env, logger } from "@oh-my-pi/pi-utils"; -import { getNativesDir } from "@oh-my-pi/pi-utils/dirs"; +import { $env, getNativesDir, logger } from "@oh-my-pi/pi-utils"; import packageJson from "../package.json"; import type { NativeBindings } from "./bindings"; import { embeddedAddon } from "./embedded-addon"; diff --git a/packages/stats/src/db.ts b/packages/stats/src/db.ts index 671f38289..5285929e3 100644 --- a/packages/stats/src/db.ts +++ b/packages/stats/src/db.ts @@ -1,6 +1,6 @@ import { Database } from "bun:sqlite"; import * as fs from "node:fs/promises"; -import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils/dirs"; +import { getConfigRootDir, getStatsDbPath } from "@oh-my-pi/pi-utils"; import type { AggregatedStats, FolderStats, diff --git a/packages/stats/src/parser.ts b/packages/stats/src/parser.ts index c287b66ac..3e78dc1f4 100644 --- a/packages/stats/src/parser.ts +++ b/packages/stats/src/parser.ts @@ -1,8 +1,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import type { AssistantMessage } from "@oh-my-pi/pi-ai"; -import { isEnoent } from "@oh-my-pi/pi-utils"; -import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs"; +import { getSessionsDir, isEnoent } from "@oh-my-pi/pi-utils"; import type { MessageStats, SessionEntry, SessionMessageEntry } from "./types"; /** diff --git a/packages/tui/README.md b/packages/tui/README.md index bf7f9114c..ed8224522 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -94,7 +94,7 @@ Container that applies padding and background color to all children. const box = new Box( 1, // paddingX (default: 1) 1, // paddingY (default: 1) - (text) => chalk.bgGray(text) // optional background function + (text) => chalk.bgGray(text), // optional background function ); box.addChild(new Text("Content")); box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically @@ -109,7 +109,7 @@ const text = new Text( "Hello World", // text content 1, // paddingX (default: 1) 1, // paddingY (default: 1) - (text) => chalk.bgGray(text) // optional background function + (text) => chalk.bgGray(text), // optional background function ); text.setText("Updated text"); text.setCustomBgFn((text) => chalk.bgBlue(text)); @@ -123,7 +123,7 @@ Single-line text that truncates to fit viewport width. Useful for status lines a const truncated = new TruncatedText( "This is a very long line that will be truncated...", 0, // paddingX (default: 0) - 0 // paddingY (default: 0) + 0, // paddingY (default: 0) ); ``` @@ -269,7 +269,7 @@ const md = new Markdown( 1, // paddingY theme, // MarkdownTheme defaultStyle, // optional DefaultTextStyle - 2 // optional code block indent (spaces) + 2, // optional code block indent (spaces) ); md.setText("Updated markdown"); ``` @@ -291,7 +291,7 @@ const loader = new Loader( tui, // TUI instance for render updates (s) => chalk.cyan(s), // spinner color function (s) => chalk.gray(s), // message color function - "Loading..." // message (default: "Loading...") + "Loading...", // message (default: "Loading...") ); loader.start(); loader.setMessage("Still loading..."); @@ -307,7 +307,7 @@ const loader = new CancellableLoader( tui, // TUI instance for render updates (s) => chalk.cyan(s), // spinner color function (s) => chalk.gray(s), // message color function - "Working..." // message + "Working...", // message ); loader.onAbort = () => done(null); // Called when user presses Escape doAsyncWork(loader.signal).then(done); @@ -345,7 +345,7 @@ const list = new SelectList( { value: "opt2", label: "Option 2", description: "Second option" }, ], 5, // maxVisible - theme // SelectListTheme + theme, // SelectListTheme ); list.onSelect = (item) => console.log("Selected:", item); @@ -390,7 +390,7 @@ const settings = new SettingsList( 10, // maxVisible theme, // SettingsListTheme (id, newValue) => console.log(`${id} changed to ${newValue}`), - () => console.log("Cancelled") + () => console.log("Cancelled"), ); settings.updateValue("theme", "light"); ``` @@ -428,7 +428,7 @@ const image = new Image( base64Data, // base64-encoded image data "image/png", // MIME type theme, // ImageTheme - options // optional ImageOptions + options, // optional ImageOptions ); tui.addChild(image); ``` @@ -443,7 +443,7 @@ Supports both slash commands and file paths. ```typescript import { CombinedAutocompleteProvider } from "@oh-my-pi/pi-tui"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; const provider = new CombinedAutocompleteProvider( [ @@ -451,7 +451,7 @@ const provider = new CombinedAutocompleteProvider( { name: "clear", description: "Clear screen" }, { name: "delete", description: "Delete last message" }, ], - getProjectDir() // base path for file completion + getProjectDir(), // base path for file completion ); editor.setAutocompleteProvider(provider); diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 874537471..8f0f2fae0 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fuzzyFind } from "@oh-my-pi/pi-natives"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]); diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 3b64c996d..52174aaa8 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1,4 +1,4 @@ -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete"; import { BracketedPasteHandler } from "../bracketed-paste"; import { type EditorKeybindingsManager, getEditorKeybindings } from "../keybindings"; diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index d016b984e..c8e9c3093 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -3,7 +3,7 @@ */ import * as fs from "node:fs"; import * as path from "node:path"; -import { getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils/dirs"; +import { getCrashLogPath, getDebugLogPath } from "@oh-my-pi/pi-utils"; import { isKeyRelease, matchesKey } from "./keys"; import type { Terminal } from "./terminal"; import { setCellDimensions, TERMINAL } from "./terminal-capabilities"; diff --git a/packages/tui/test/chat-simple.ts b/packages/tui/test/chat-simple.ts index 66742b716..32e0790e2 100644 --- a/packages/tui/test/chat-simple.ts +++ b/packages/tui/test/chat-simple.ts @@ -8,7 +8,7 @@ import { Markdown } from "@oh-my-pi/pi-tui/components/markdown"; import { Text } from "@oh-my-pi/pi-tui/components/text"; import { ProcessTerminal } from "@oh-my-pi/pi-tui/terminal"; import { TUI } from "@oh-my-pi/pi-tui/tui"; -import { getProjectDir } from "@oh-my-pi/pi-utils/dirs"; +import { getProjectDir } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { defaultEditorTheme, defaultMarkdownTheme } from "./test-themes"; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 05e302467..d5e108d0a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,11 +1,13 @@ export * from "./abortable"; export * from "./async"; export * from "./color"; +export * from "./dirs"; export * from "./env"; export * from "./format"; export * from "./fs-error"; export * from "./glob"; export * from "./indent"; +export * from "./json"; export * as logger from "./logger"; export * as postmortem from "./postmortem"; export * as procmgr from "./procmgr"; diff --git a/packages/utils/src/json.ts b/packages/utils/src/json.ts new file mode 100644 index 000000000..8d7bc7744 --- /dev/null +++ b/packages/utils/src/json.ts @@ -0,0 +1,10 @@ +/** + * Try to parse JSON, returning null on failure. + */ +export function tryParseJson(content: string): T | null { + try { + return JSON.parse(content) as T; + } catch { + return null; + } +}