From 008c76f955ae95b6a15703064cc313fdd7b0fde0 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 18 Jun 2026 10:27:11 +0200 Subject: [PATCH] feat(coding-agent): export project config dir name --- packages/coding-agent/CHANGELOG.md | 8 ++++++++ packages/coding-agent/docs/extensions.md | 14 ++++++++++++++ packages/coding-agent/docs/sdk.md | 3 ++- .../coding-agent/examples/extensions/preset.ts | 14 ++++++++++---- .../examples/extensions/provider-payload.ts | 10 +++++----- .../examples/extensions/sandbox/index.ts | 4 ++-- .../examples/extensions/subagent/agents.ts | 4 ++-- .../examples/extensions/subagent/index.ts | 12 +++++++++--- packages/coding-agent/src/core/project-trust.ts | 3 ++- packages/coding-agent/src/index.ts | 10 +++++++++- .../interactive/components/config-selector.ts | 10 +++++----- .../src/modes/interactive/interactive-mode.ts | 3 ++- packages/coding-agent/src/package-manager-cli.ts | 5 +++-- 13 files changed, 73 insertions(+), 27 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3bbc9cbd..716b210f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi`. + +### Changed + +- Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. + ### Fixed - Fixed `/model` autocomplete and model selection searches to match provider/model queries regardless of whether the provider or model token is typed first. diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index ab3188ce..5a559813 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -898,6 +898,20 @@ Current run mode: `"tui"`, `"rpc"`, `"json"`, or `"print"`. Use `ctx.mode === "t Current working directory. +Use `CONFIG_DIR_NAME` instead of hardcoding `.pi` when constructing project-local config paths. Rebranded distributions can use a different config directory name. + +```typescript +import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { join } from "node:path"; + +export default function (pi: ExtensionAPI) { + pi.on("session_start", (_event, ctx) => { + const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json"); + // ... + }); +} +``` + ### ctx.isProjectTrusted() Returns whether project-local trust is active for the current session context. This includes temporary trust decisions and CLI trust overrides, not just saved decisions in the global trust store. diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index c6b2a75d..0c521e74 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -1110,7 +1110,8 @@ DefaultResourceLoader type ResourceLoader createEventBus -// Helpers +// Constants and helpers +CONFIG_DIR_NAME defineTool getAgentDir getPackageDir diff --git a/packages/coding-agent/examples/extensions/preset.ts b/packages/coding-agent/examples/extensions/preset.ts index 92224ec2..b78237ca 100644 --- a/packages/coding-agent/examples/extensions/preset.ts +++ b/packages/coding-agent/examples/extensions/preset.ts @@ -42,7 +42,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import type { Api, Model } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent"; +import { CONFIG_DIR_NAME, DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent"; import { Container, Key, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui"; // Preset configuration @@ -69,7 +69,7 @@ interface PresetsConfig { */ function loadPresets(cwd: string): PresetsConfig { const globalPath = join(getAgentDir(), "presets.json"); - const projectPath = join(cwd, ".pi", "presets.json"); + const projectPath = join(cwd, CONFIG_DIR_NAME, "presets.json"); let globalPresets: PresetsConfig = {}; let projectPresets: PresetsConfig = {}; @@ -200,7 +200,10 @@ export default function presetExtension(pi: ExtensionAPI) { const presetNames = Object.keys(presets); if (presetNames.length === 0) { - ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning"); + ctx.ui.notify( + `No presets defined. Add presets to ${join(getAgentDir(), "presets.json")} or ${join(ctx.cwd, CONFIG_DIR_NAME, "presets.json")}`, + "warning", + ); return; } @@ -308,7 +311,10 @@ export default function presetExtension(pi: ExtensionAPI) { async function cyclePreset(ctx: ExtensionContext): Promise { const presetNames = getPresetOrder(); if (presetNames.length === 0) { - ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning"); + ctx.ui.notify( + `No presets defined. Add presets to ${join(getAgentDir(), "presets.json")} or ${join(ctx.cwd, CONFIG_DIR_NAME, "presets.json")}`, + "warning", + ); return; } diff --git a/packages/coding-agent/examples/extensions/provider-payload.ts b/packages/coding-agent/examples/extensions/provider-payload.ts index 860ddc00..7f02a077 100644 --- a/packages/coding-agent/examples/extensions/provider-payload.ts +++ b/packages/coding-agent/examples/extensions/provider-payload.ts @@ -1,18 +1,18 @@ import { appendFileSync } from "node:fs"; import { join } from "node:path"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; export default function (pi: ExtensionAPI) { - const logFile = join(process.cwd(), ".pi", "provider-payload.log"); - - pi.on("before_provider_request", (event) => { + pi.on("before_provider_request", (event, ctx) => { + const logFile = join(ctx.cwd, CONFIG_DIR_NAME, "provider-payload.log"); appendFileSync(logFile, `${JSON.stringify(event.payload, null, 2)}\n\n`, "utf8"); // Optional: replace the payload instead of only logging it. // return { ...event.payload, temperature: 0 }; }); - pi.on("after_provider_response", (event) => { + pi.on("after_provider_response", (event, ctx) => { + const logFile = join(ctx.cwd, CONFIG_DIR_NAME, "provider-payload.log"); appendFileSync(logFile, `[${event.status}] ${JSON.stringify(event.headers)}\n\n`, "utf8"); }); } diff --git a/packages/coding-agent/examples/extensions/sandbox/index.ts b/packages/coding-agent/examples/extensions/sandbox/index.ts index 94f8f1cf..b54d75d1 100644 --- a/packages/coding-agent/examples/extensions/sandbox/index.ts +++ b/packages/coding-agent/examples/extensions/sandbox/index.ts @@ -46,7 +46,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { type BashOperations, createBashTool, getAgentDir } from "@earendil-works/pi-coding-agent"; +import { type BashOperations, CONFIG_DIR_NAME, createBashTool, getAgentDir } from "@earendil-works/pi-coding-agent"; interface SandboxConfig extends SandboxRuntimeConfig { enabled?: boolean; @@ -77,7 +77,7 @@ const DEFAULT_CONFIG: SandboxConfig = { }; function loadConfig(cwd: string): SandboxConfig { - const projectConfigPath = join(cwd, ".pi", "sandbox.json"); + const projectConfigPath = join(cwd, CONFIG_DIR_NAME, "sandbox.json"); const globalConfigPath = join(getAgentDir(), "extensions", "sandbox.json"); let globalConfig: Partial = {}; diff --git a/packages/coding-agent/examples/extensions/subagent/agents.ts b/packages/coding-agent/examples/extensions/subagent/agents.ts index eab6c630..c41ef579 100644 --- a/packages/coding-agent/examples/extensions/subagent/agents.ts +++ b/packages/coding-agent/examples/extensions/subagent/agents.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; +import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; export type AgentScope = "user" | "project" | "both"; @@ -85,7 +85,7 @@ function isDirectory(p: string): boolean { function findNearestProjectAgentsDir(cwd: string): string | null { let currentDir = cwd; while (true) { - const candidate = path.join(currentDir, ".pi", "agents"); + const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents"); if (isDirectory(candidate)) return candidate; const parentDir = path.dirname(currentDir); diff --git a/packages/coding-agent/examples/extensions/subagent/index.ts b/packages/coding-agent/examples/extensions/subagent/index.ts index 92b471a3..832dcc74 100644 --- a/packages/coding-agent/examples/extensions/subagent/index.ts +++ b/packages/coding-agent/examples/extensions/subagent/index.ts @@ -19,7 +19,13 @@ import * as path from "node:path"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { Message } from "@earendil-works/pi-ai"; import { StringEnum } from "@earendil-works/pi-ai"; -import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent"; +import { + CONFIG_DIR_NAME, + type ExtensionAPI, + getAgentDir, + getMarkdownTheme, + withFileMutationQueue, +} from "@earendil-works/pi-coding-agent"; import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts"; @@ -458,8 +464,8 @@ export default function (pi: ExtensionAPI) { description: [ "Delegate tasks to specialized subagents with isolated context.", "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).", - 'Default agent scope is "user" (from ~/.pi/agent/agents).', - 'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").', + `Default agent scope is "user" (from ${path.join(getAgentDir(), "agents")}).`, + `To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" (or "project").`, ].join(" "), parameters: SubagentParams, diff --git a/packages/coding-agent/src/core/project-trust.ts b/packages/coding-agent/src/core/project-trust.ts index c0892439..2521ad5d 100644 --- a/packages/coding-agent/src/core/project-trust.ts +++ b/packages/coding-agent/src/core/project-trust.ts @@ -1,3 +1,4 @@ +import { CONFIG_DIR_NAME } from "../config.ts"; import { emitProjectTrustEvent } from "./extensions/runner.ts"; import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/types.ts"; import type { DefaultProjectTrust } from "./settings-manager.ts"; @@ -21,7 +22,7 @@ export interface ResolveProjectTrustedOptions { } function formatProjectTrustPrompt(cwd: string): string { - return `Trust project folder?\n${cwd}\n\nThis allows pi to load .pi settings and resources, install missing project packages, and execute project extensions.`; + return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`; } async function selectProjectTrustOption( diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 3176a167..70416af1 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -3,7 +3,15 @@ export { type Args, parseArgs } from "./cli/args.ts"; // Config paths -export { getAgentDir, getDocsPath, getExamplesPath, getPackageDir, getReadmePath, VERSION } from "./config.ts"; +export { + CONFIG_DIR_NAME, + getAgentDir, + getDocsPath, + getExamplesPath, + getPackageDir, + getReadmePath, + VERSION, +} from "./config.ts"; export { AgentSession, type AgentSessionConfig, diff --git a/packages/coding-agent/src/modes/interactive/components/config-selector.ts b/packages/coding-agent/src/modes/interactive/components/config-selector.ts index 93ef4bea..7c46841d 100644 --- a/packages/coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/config-selector.ts @@ -73,7 +73,7 @@ function formatBaseDir(baseDir: string): string { return displayPath.endsWith("/") ? displayPath : `${displayPath}/`; } -function getGroupLabel(metadata: PathMetadata): string { +function getGroupLabel(metadata: PathMetadata, agentDir: string): string { if (metadata.origin === "package") { return `${metadata.source} (${metadata.scope})`; } @@ -84,12 +84,12 @@ function getGroupLabel(metadata: PathMetadata): string { ? `User (${formatBaseDir(metadata.baseDir)})` : `Project (${formatBaseDir(metadata.baseDir)})`; } - return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)"; + return metadata.scope === "user" ? `User (${formatBaseDir(agentDir)})` : `Project (${CONFIG_DIR_NAME}/)`; } return metadata.scope === "user" ? "User settings" : "Project settings"; } -function buildGroups(resolved: ResolvedPaths): ResourceGroup[] { +function buildGroups(resolved: ResolvedPaths, agentDir: string): ResourceGroup[] { const groupMap = new Map(); const addToGroup = (resources: ResolvedResource[], resourceType: ResourceType) => { @@ -100,7 +100,7 @@ function buildGroups(resolved: ResolvedPaths): ResourceGroup[] { if (!groupMap.has(groupKey)) { groupMap.set(groupKey, { key: groupKey, - label: getGroupLabel(metadata), + label: getGroupLabel(metadata, agentDir), scope: metadata.scope, origin: metadata.origin, source: metadata.source, @@ -601,7 +601,7 @@ export class ConfigSelectorComponent extends Container implements Focusable { ) { super(); - const groups = buildGroups(resolvedPaths); + const groups = buildGroups(resolvedPaths, agentDir); // Add header this.addChild(new Spacer(1)); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c2dc7875..ad9d3d3a 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -52,6 +52,7 @@ import { spawn, spawnSync } from "child_process"; import { APP_NAME, APP_TITLE, + CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, @@ -3307,7 +3308,7 @@ export class InteractiveMode { new Text( theme.fg( "warning", - "This project is not trusted. Project .pi resources and packages are ignored. Use /trust to save a trust decision, then restart pi.", + `This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart pi.`, ), 1, 0, diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 0dbe750c..e00de1b4 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -4,6 +4,7 @@ import { selectConfig } from "./cli/config-selector.ts"; import { createProjectTrustContext } from "./cli/project-trust.ts"; import { APP_NAME, + CONFIG_DIR_NAME, detectInstallMethod, getAgentDir, getPackageDir, @@ -93,7 +94,7 @@ function printPackageCommandHelp(command: PackageCommand): void { Install a package and add it to settings. Options: - -l, --local Install project-locally (.pi/settings.json) + -l, --local Install project-locally (${CONFIG_DIR_NAME}/settings.json) -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command @@ -115,7 +116,7 @@ Remove a package and its source from settings. Alias: ${APP_NAME} uninstall [-l] Options: - -l, --local Remove from project settings (.pi/settings.json) + -l, --local Remove from project settings (${CONFIG_DIR_NAME}/settings.json) -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command