feat(coding-agent): export project config dir name
Some checks are pending
CI / build-check-test (push) Waiting to run

This commit is contained in:
Vegard Stikbakke 2026-06-18 10:27:11 +02:00
parent 58dd2f5996
commit 008c76f955
13 changed files with 73 additions and 27 deletions

View file

@ -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.

View file

@ -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.

View file

@ -1110,7 +1110,8 @@ DefaultResourceLoader
type ResourceLoader
createEventBus
// Helpers
// Constants and helpers
CONFIG_DIR_NAME
defineTool
getAgentDir
getPackageDir

View file

@ -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<void> {
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;
}

View file

@ -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");
});
}

View file

@ -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<SandboxConfig> = {};

View file

@ -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);

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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<string, ResourceGroup>();
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));

View file

@ -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,

View file

@ -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 <source> [-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