refactor: migrated imports to unified package root and consolidated skill discovery logic
- Consolidated @oh-my-pi/pi-utils subpath imports into single package root import across 100+ files. - Moved tryParseJson utility from local web scrapers module to @oh-my-pi/pi-utils package for centralized JSON parsing. - Renamed loadSkillsFromDir to scanSkillsFromDir and refactored skill discovery to use fs.promises.readdir instead of glob-based approach. - Replaced custom parseJSON with tryParseJson across discovery modules for consistent error handling. - Removed emitCustomToolSessionEvent method and cleanupSshResources function, consolidating shutdown logic into dispose method. - Updated glob pattern construction to use GlobBuilder with literal_separator(true) for improved path handling.
This commit is contained in:
@@ -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<GlobSet> {
|
||||
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
|
||||
|
||||
+8
-7
@@ -30,7 +30,7 @@ For provider-based discovery (native/Claude/Codex/Agents/plugin providers), skil
|
||||
|
||||
Nested patterns like `<skills-root>/group/<skill>/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: `<skills-root>/<skill-name>/SKILL.md`
|
||||
- Always include explicit `name` and `description` frontmatter
|
||||
- Keep referenced assets under the same skill directory and access with `skill://<name>/...`
|
||||
- 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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -623,14 +623,14 @@ Normalization and collision handling:
|
||||
- `source = "<provider>:<level>",`
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles `omp plugin <command>` 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";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles `omp setup <component>` 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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles `omp ssh <command>` 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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<LoadResult<Skill>> {
|
||||
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",
|
||||
|
||||
@@ -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<LoadResult<MCPServer>>
|
||||
|
||||
const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => {
|
||||
const result: MCPServer[] = [];
|
||||
const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
const data = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
if (!data?.mcpServers) return result;
|
||||
|
||||
const expanded = expandEnvVarsDeep(data.mcpServers);
|
||||
@@ -245,7 +244,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
||||
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<LoadResult<Extens
|
||||
if (!settingsContent) continue;
|
||||
|
||||
const settingsPath = path.join(dir, "settings.json");
|
||||
const settingsData = parseJSON<{ extensions?: unknown }>(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<LoadResult<Extension>>
|
||||
if (!content) continue;
|
||||
|
||||
const { extDir, manifestPath, entryName, level } = manifestCandidates[i];
|
||||
const manifest = parseJSON<ExtensionManifest>(content);
|
||||
const manifest = tryParseJson<ExtensionManifest>(content);
|
||||
if (!manifest) {
|
||||
warnings.push(`Failed to parse ${manifestPath}`);
|
||||
continue;
|
||||
@@ -655,7 +654,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
||||
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<LoadResult<Settings>> {
|
||||
const content = await readFile(settingsPath);
|
||||
if (!content) continue;
|
||||
|
||||
const data = parseJSON<Record<string, unknown>>(content);
|
||||
const data = tryParseJson<Record<string, unknown>>(content);
|
||||
if (!data) {
|
||||
warnings.push(`Failed to parse ${settingsPath}`);
|
||||
continue;
|
||||
|
||||
@@ -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<LoadResult<Skill>> {
|
||||
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,
|
||||
|
||||
@@ -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<LoadResult<MCPServer>>
|
||||
|
||||
const parseMcpServers = (content: string | null, path: string, level: "user" | "project"): MCPServer[] => {
|
||||
if (!content) return [];
|
||||
const json = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
const json = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
if (!json?.mcpServers) return [];
|
||||
|
||||
const mcpServers = expandEnvVarsDeep(json.mcpServers);
|
||||
@@ -163,8 +163,8 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
||||
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<LoadResult<Settings>> {
|
||||
|
||||
const userContent = await readFile(userSettingsJson);
|
||||
if (userContent) {
|
||||
const data = parseJSON<Record<string, unknown>>(userContent);
|
||||
const data = tryParseJson<Record<string, unknown>>(userContent);
|
||||
if (data) {
|
||||
items.push({
|
||||
path: userSettingsJson,
|
||||
@@ -413,7 +413,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
||||
const projectSettingsJson = path.join(projectBase, "settings.json");
|
||||
const projectContent = await readFile(projectSettingsJson);
|
||||
if (projectContent) {
|
||||
const data = parseJSON<Record<string, unknown>>(projectContent);
|
||||
const data = tryParseJson<Record<string, unknown>>(projectContent);
|
||||
if (data) {
|
||||
items.push({
|
||||
path: projectSettingsJson,
|
||||
|
||||
@@ -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<LoadResult<Skill>> {
|
||||
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",
|
||||
|
||||
@@ -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<string, unknown> }>(content);
|
||||
const parsed = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
if (!parsed?.mcpServers) {
|
||||
return { items, warning: `${path}: missing or invalid 'mcpServers' key` };
|
||||
}
|
||||
@@ -158,7 +159,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
||||
const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null);
|
||||
|
||||
if (userContent && userPath) {
|
||||
const parsed = parseJSON<Record<string, unknown>>(userContent);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(userContent);
|
||||
if (parsed) {
|
||||
items.push({
|
||||
path: userPath,
|
||||
@@ -173,7 +174,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
||||
|
||||
const projectContent = await projectContentPromise;
|
||||
if (projectContent && projectPath) {
|
||||
const parsed = parseJSON<Record<string, unknown>>(projectContent);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(projectContent);
|
||||
if (parsed) {
|
||||
items.push({
|
||||
path: projectPath,
|
||||
|
||||
@@ -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<string, unknown> }>(content);
|
||||
const parsed = tryParseJson<{ mcpServers?: Record<string, unknown> }>(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<ExtensionManifest>(content);
|
||||
const manifest = tryParseJson<ExtensionManifest>(content);
|
||||
if (!manifest) {
|
||||
warnings.push(`Invalid JSON in ${manifestPath}`);
|
||||
continue;
|
||||
@@ -268,7 +268,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
||||
if (userPath) {
|
||||
const content = await readFile(userPath);
|
||||
if (content) {
|
||||
const parsed = parseJSON<Record<string, unknown>>(content);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(content);
|
||||
if (parsed) {
|
||||
items.push({
|
||||
path: userPath,
|
||||
@@ -287,7 +287,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
||||
if (projectPath) {
|
||||
const content = await readFile(projectPath);
|
||||
if (content) {
|
||||
const parsed = parseJSON<Record<string, unknown>>(content);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(content);
|
||||
if (parsed) {
|
||||
items.push({
|
||||
path: projectPath,
|
||||
|
||||
@@ -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<LoadResult<Skill>> {
|
||||
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 <dir>/<subdir>/SKILL.md
|
||||
const discoveredMatches = new Set<string>();
|
||||
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: "<skilldir>/SKILL.md" -> "<skilldir>"
|
||||
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, string>): string {
|
||||
function expandEnvVars(value: string, extraEnv?: Record<string, string>): 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<T>(
|
||||
return { items, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON safely.
|
||||
*/
|
||||
export function parseJSON<T>(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<ClaudePluginsRegistry>(content);
|
||||
const data = tryParseJson<ClaudePluginsRegistry>(content);
|
||||
if (!data || typeof data !== "object") return null;
|
||||
if (
|
||||
typeof data.version !== "number" ||
|
||||
|
||||
@@ -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<MCPConfigFile>(content);
|
||||
const config = tryParseJson<MCPConfigFile>(content);
|
||||
if (!config) {
|
||||
warnings.push(`Failed to parse JSON in ${path}`);
|
||||
return { items, warnings };
|
||||
|
||||
@@ -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<Record<string, unknow
|
||||
const content = await readFile(configPath);
|
||||
if (!content) return null;
|
||||
|
||||
const parsed = parseJSON<Record<string, unknown>>(content);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(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<LoadResult<Skill>> {
|
||||
|
||||
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<LoadResult<Skill>> {
|
||||
|
||||
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<LoadResult<Settings>> {
|
||||
if (userConfigPath) {
|
||||
const content = await readFile(userConfigPath);
|
||||
if (content) {
|
||||
const parsed = parseJSON<Record<string, unknown>>(content);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(content);
|
||||
if (parsed) {
|
||||
items.push({
|
||||
path: userConfigPath,
|
||||
@@ -325,7 +324,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
||||
const projectConfigPath = path.join(ctx.cwd, "opencode.json");
|
||||
const content = await readFile(projectConfigPath);
|
||||
if (content) {
|
||||
const parsed = parseJSON<Record<string, unknown>>(content);
|
||||
const parsed = tryParseJson<Record<string, unknown>>(content);
|
||||
if (parsed) {
|
||||
items.push({
|
||||
path: projectConfigPath,
|
||||
|
||||
@@ -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<SSHConfigFile>(content);
|
||||
const parsed = tryParseJson<SSHConfigFile>(content);
|
||||
if (!parsed) {
|
||||
warnings.push(`Failed to parse JSON in ${filePath}`);
|
||||
return { items, warnings };
|
||||
|
||||
@@ -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<string, unknown> } }>(content);
|
||||
const parsed = tryParseJson<{ mcp?: { servers?: Record<string, unknown> } }>(content);
|
||||
if (!parsed) {
|
||||
warnings.push(`Invalid JSON in ${path}`);
|
||||
return { items, warnings };
|
||||
|
||||
@@ -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<LoadResult<MCPServer>>
|
||||
for (const { content, path, scope } of configs) {
|
||||
if (!content || !path) continue;
|
||||
|
||||
const config = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
const config = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
|
||||
if (!config?.mcpServers) continue;
|
||||
|
||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null> {
|
||||
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<LoadSkillsResult> {
|
||||
const skills: Skill[] = [];
|
||||
const warnings: SkillWarning[] = [];
|
||||
const seenPaths = new Set<string>();
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<LoadS
|
||||
message: `name collision: "${capSkill.name}" already loaded from ${existing.filePath}, skipping this one`,
|
||||
});
|
||||
} else {
|
||||
// Transform capability skill to legacy format
|
||||
const skill: Skill = {
|
||||
skillMap.set(capSkill.name, {
|
||||
name: capSkill.name,
|
||||
description: capSkill.frontmatter?.description || "",
|
||||
description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
|
||||
filePath: capSkill.path,
|
||||
baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
|
||||
source: `${capSkill._source.provider}:${capSkill.level}`,
|
||||
_source: capSkill._source,
|
||||
};
|
||||
skillMap.set(capSkill.name, skill);
|
||||
});
|
||||
realPathSet.add(resolvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Process custom directories - scan directly without using full provider system
|
||||
const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
|
||||
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<LoadS
|
||||
|
||||
return {
|
||||
skills: Array.from(skillMap.values()),
|
||||
warnings: [...result.warnings.map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
|
||||
warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<number, { data: string; mimeType: string }> = new Map();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
if (reason !== "shutdown") return;
|
||||
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
||||
await this.#extensionRunner.emit({ type: "session_shutdown" });
|
||||
}
|
||||
await cleanupSshResources();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<void> {
|
||||
const key = host.name;
|
||||
const pending = pendingConnections.get(key);
|
||||
@@ -375,6 +376,13 @@ export async function ensureConnection(host: SSHConnectionTarget): Promise<void>
|
||||
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) {
|
||||
|
||||
@@ -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<boolean> {
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
export async function mountRemote(host: SSHConnectionTarget, remotePath = "/"): Promise<string | undefined> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -59,7 +59,6 @@ export interface QuestionResult {
|
||||
}
|
||||
|
||||
export interface AskToolDetails {
|
||||
/** Single question mode (backwards compatible) */
|
||||
question?: string;
|
||||
options?: string[];
|
||||
multi?: boolean;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user