fix(coding-agent): attach source info to resources and commands, fixes #1734
This commit is contained in:
@@ -75,7 +75,7 @@ import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.j
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js";
|
||||
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from "./session-manager.js";
|
||||
import type { SettingsManager } from "./settings-manager.js";
|
||||
import { BUILTIN_SLASH_COMMANDS, type SlashCommandInfo, type SlashCommandLocation } from "./slash-commands.js";
|
||||
import type { SlashCommandInfo, SlashCommandLocation } from "./slash-commands.js";
|
||||
import { buildSystemPrompt } from "./system-prompt.js";
|
||||
import type { BashOperations } from "./tools/bash.js";
|
||||
import { createAllToolDefinitions } from "./tools/index.js";
|
||||
@@ -2091,23 +2091,20 @@ export class AgentSession {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const reservedBuiltins = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
|
||||
|
||||
const getCommands = (): SlashCommandInfo[] => {
|
||||
const extensionCommands: SlashCommandInfo[] = runner
|
||||
.getRegisteredCommandsWithPaths()
|
||||
.filter(({ command }) => !reservedBuiltins.has(command.name))
|
||||
.map(({ command, extensionPath }) => ({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
source: "extension",
|
||||
path: extensionPath,
|
||||
}));
|
||||
const extensionCommands: SlashCommandInfo[] = runner.getRegisteredCommands().map((command) => ({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
source: "extension",
|
||||
sourceInfo: command.sourceInfo,
|
||||
path: command.extensionPath,
|
||||
}));
|
||||
|
||||
const templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
source: "prompt",
|
||||
sourceInfo: template.sourceInfo,
|
||||
location: normalizeLocation(template.source),
|
||||
path: template.filePath,
|
||||
}));
|
||||
@@ -2116,6 +2113,7 @@ export class AgentSession {
|
||||
name: `skill:${skill.name}`,
|
||||
description: skill.description,
|
||||
source: "skill",
|
||||
sourceInfo: skill.sourceInfo,
|
||||
location: normalizeLocation(skill.source),
|
||||
path: skill.filePath,
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
export type { SlashCommandInfo, SlashCommandLocation, SlashCommandSource } from "../slash-commands.js";
|
||||
export type { SourceInfo } from "../source-info.js";
|
||||
export {
|
||||
createExtensionRuntime,
|
||||
discoverAndLoadExtensions,
|
||||
|
||||
@@ -179,8 +179,13 @@ function createExtensionAPI(
|
||||
runtime.refreshTools();
|
||||
},
|
||||
|
||||
registerCommand(name: string, options: Omit<RegisteredCommand, "name">): void {
|
||||
extension.commands.set(name, { name, ...options });
|
||||
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "extensionPath">): void {
|
||||
extension.commands.set(name, {
|
||||
name,
|
||||
extensionPath: extension.path,
|
||||
sourceInfo: extension.sourceInfo,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
|
||||
registerShortcut(
|
||||
|
||||
@@ -467,27 +467,16 @@ export class ExtensionRunner {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
|
||||
this.commandDiagnostics = [];
|
||||
|
||||
private collectRegisteredCommands(diagnostics?: ResourceDiagnostic[]): RegisteredCommand[] {
|
||||
const commands: RegisteredCommand[] = [];
|
||||
const commandOwners = new Map<string, string>();
|
||||
for (const ext of this.extensions) {
|
||||
for (const command of ext.commands.values()) {
|
||||
if (reserved?.has(command.name)) {
|
||||
const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`;
|
||||
this.commandDiagnostics.push({ type: "warning", message, path: ext.path });
|
||||
if (!this.hasUI()) {
|
||||
console.warn(message);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingOwner = commandOwners.get(command.name);
|
||||
if (existingOwner) {
|
||||
const message = `Extension command '${command.name}' from ${ext.path} conflicts with ${existingOwner}. Skipping.`;
|
||||
this.commandDiagnostics.push({ type: "warning", message, path: ext.path });
|
||||
if (!this.hasUI()) {
|
||||
diagnostics?.push({ type: "warning", message, path: ext.path });
|
||||
if (diagnostics && !this.hasUI()) {
|
||||
console.warn(message);
|
||||
}
|
||||
continue;
|
||||
@@ -500,28 +489,19 @@ export class ExtensionRunner {
|
||||
return commands;
|
||||
}
|
||||
|
||||
getRegisteredCommands(): RegisteredCommand[] {
|
||||
const diagnostics: ResourceDiagnostic[] = [];
|
||||
const commands = this.collectRegisteredCommands(diagnostics);
|
||||
this.commandDiagnostics = diagnostics;
|
||||
return commands;
|
||||
}
|
||||
|
||||
getCommandDiagnostics(): ResourceDiagnostic[] {
|
||||
return this.commandDiagnostics;
|
||||
}
|
||||
|
||||
getRegisteredCommandsWithPaths(): Array<{ command: RegisteredCommand; extensionPath: string }> {
|
||||
const result: Array<{ command: RegisteredCommand; extensionPath: string }> = [];
|
||||
for (const ext of this.extensions) {
|
||||
for (const command of ext.commands.values()) {
|
||||
result.push({ command, extensionPath: ext.path });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getCommand(name: string): RegisteredCommand | undefined {
|
||||
for (const ext of this.extensions) {
|
||||
const command = ext.commands.get(name);
|
||||
if (command) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return this.collectRegisteredCommands().find((command) => command.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,6 +55,7 @@ import type {
|
||||
SessionManager,
|
||||
} from "../session-manager.js";
|
||||
import type { SlashCommandInfo } from "../slash-commands.js";
|
||||
import type { SourceInfo } from "../source-info.js";
|
||||
import type { BashOperations } from "../tools/bash.js";
|
||||
import type { EditToolDetails } from "../tools/edit.js";
|
||||
import type {
|
||||
@@ -960,6 +961,8 @@ export type MessageRenderer<T = unknown> = (
|
||||
|
||||
export interface RegisteredCommand {
|
||||
name: string;
|
||||
extensionPath: string;
|
||||
sourceInfo?: SourceInfo;
|
||||
description?: string;
|
||||
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
|
||||
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
||||
@@ -1035,7 +1038,7 @@ export interface ExtensionAPI {
|
||||
// =========================================================================
|
||||
|
||||
/** Register a custom command. */
|
||||
registerCommand(name: string, options: Omit<RegisteredCommand, "name">): void;
|
||||
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "extensionPath">): void;
|
||||
|
||||
/** Register a keyboard shortcut. */
|
||||
registerShortcut(
|
||||
@@ -1414,6 +1417,7 @@ export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionAction
|
||||
export interface Extension {
|
||||
path: string;
|
||||
resolvedPath: string;
|
||||
sourceInfo?: SourceInfo;
|
||||
handlers: Map<string, HandlerFn[]>;
|
||||
tools: Map<string, RegisteredTool>;
|
||||
messageRenderers: Map<string, MessageRenderer>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { homedir } from "os";
|
||||
import { basename, isAbsolute, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import type { SourceInfo } from "./source-info.js";
|
||||
|
||||
/**
|
||||
* Represents a prompt template loaded from a markdown file
|
||||
@@ -12,6 +13,7 @@ export interface PromptTemplate {
|
||||
description: string;
|
||||
content: string;
|
||||
source: string; // "user", "project", or "path"
|
||||
sourceInfo?: SourceInfo;
|
||||
filePath: string; // Absolute path to the template file
|
||||
}
|
||||
|
||||
@@ -99,7 +101,7 @@ export function substituteArgs(content: string, args: string[]): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadTemplateFromFile(filePath: string, source: string, sourceLabel: string): PromptTemplate | null {
|
||||
function loadTemplateFromFile(filePath: string, source: string): PromptTemplate | null {
|
||||
try {
|
||||
const rawContent = readFileSync(filePath, "utf-8");
|
||||
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(rawContent);
|
||||
@@ -117,9 +119,6 @@ function loadTemplateFromFile(filePath: string, source: string, sourceLabel: str
|
||||
}
|
||||
}
|
||||
|
||||
// Append source to description
|
||||
description = description ? `${description} ${sourceLabel}` : sourceLabel;
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
@@ -135,7 +134,7 @@ function loadTemplateFromFile(filePath: string, source: string, sourceLabel: str
|
||||
/**
|
||||
* Scan a directory for .md files (non-recursive) and load them as prompt templates.
|
||||
*/
|
||||
function loadTemplatesFromDir(dir: string, source: string, sourceLabel: string): PromptTemplate[] {
|
||||
function loadTemplatesFromDir(dir: string, source: string): PromptTemplate[] {
|
||||
const templates: PromptTemplate[] = [];
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
@@ -161,7 +160,7 @@ function loadTemplatesFromDir(dir: string, source: string, sourceLabel: string):
|
||||
}
|
||||
|
||||
if (isFile && entry.name.endsWith(".md")) {
|
||||
const template = loadTemplateFromFile(fullPath, source, sourceLabel);
|
||||
const template = loadTemplateFromFile(fullPath, source);
|
||||
if (template) {
|
||||
templates.push(template);
|
||||
}
|
||||
@@ -198,11 +197,6 @@ function resolvePromptPath(p: string, cwd: string): string {
|
||||
return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
|
||||
}
|
||||
|
||||
function buildPathSourceLabel(p: string): string {
|
||||
const base = basename(p).replace(/\.md$/, "") || "path";
|
||||
return `(path:${base})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all prompt templates from:
|
||||
* 1. Global: agentDir/prompts/
|
||||
@@ -221,11 +215,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
||||
// 1. Load global templates from agentDir/prompts/
|
||||
// Note: if agentDir is provided, it should be the agent dir, not the prompts dir
|
||||
const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
||||
templates.push(...loadTemplatesFromDir(globalPromptsDir, "user", "(user)"));
|
||||
templates.push(...loadTemplatesFromDir(globalPromptsDir, "user"));
|
||||
|
||||
// 2. Load project templates from cwd/{CONFIG_DIR_NAME}/prompts/
|
||||
const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
|
||||
templates.push(...loadTemplatesFromDir(projectPromptsDir, "project", "(project)"));
|
||||
templates.push(...loadTemplatesFromDir(projectPromptsDir, "project"));
|
||||
}
|
||||
|
||||
const userPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir;
|
||||
@@ -240,16 +234,16 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
||||
return target.startsWith(prefix);
|
||||
};
|
||||
|
||||
const getSourceInfo = (resolvedPath: string): { source: string; label: string } => {
|
||||
const getSource = (resolvedPath: string): string => {
|
||||
if (!includeDefaults) {
|
||||
if (isUnderPath(resolvedPath, userPromptsDir)) {
|
||||
return { source: "user", label: "(user)" };
|
||||
return "user";
|
||||
}
|
||||
if (isUnderPath(resolvedPath, projectPromptsDir)) {
|
||||
return { source: "project", label: "(project)" };
|
||||
return "project";
|
||||
}
|
||||
}
|
||||
return { source: "path", label: buildPathSourceLabel(resolvedPath) };
|
||||
return "path";
|
||||
};
|
||||
|
||||
// 3. Load explicit prompt paths
|
||||
@@ -261,11 +255,11 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
||||
|
||||
try {
|
||||
const stats = statSync(resolvedPath);
|
||||
const { source, label } = getSourceInfo(resolvedPath);
|
||||
const source = getSource(resolvedPath);
|
||||
if (stats.isDirectory()) {
|
||||
templates.push(...loadTemplatesFromDir(resolvedPath, source, label));
|
||||
templates.push(...loadTemplatesFromDir(resolvedPath, source));
|
||||
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
||||
const template = loadTemplateFromFile(resolvedPath, source, label);
|
||||
const template = loadTemplateFromFile(resolvedPath, source);
|
||||
if (template) {
|
||||
templates.push(template);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { loadPromptTemplates } from "./prompt-templates.js";
|
||||
import { SettingsManager } from "./settings-manager.js";
|
||||
import type { Skill } from "./skills.js";
|
||||
import { loadSkills } from "./skills.js";
|
||||
import { createSourceInfo, type SourceInfo } from "./source-info.js";
|
||||
|
||||
export interface ResourceExtensionPaths {
|
||||
skillPaths?: Array<{ path: string; metadata: PathMetadata }>;
|
||||
@@ -32,7 +33,6 @@ export interface ResourceLoader {
|
||||
getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> };
|
||||
getSystemPrompt(): string | undefined;
|
||||
getAppendSystemPrompt(): string[];
|
||||
getPathMetadata(): Map<string, PathMetadata>;
|
||||
extendResources(paths: ResourceExtensionPaths): void;
|
||||
reload(): Promise<void>;
|
||||
}
|
||||
@@ -193,8 +193,10 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
private agentsFiles: Array<{ path: string; content: string }>;
|
||||
private systemPrompt?: string;
|
||||
private appendSystemPrompt: string[];
|
||||
private pathMetadata: Map<string, PathMetadata>;
|
||||
private lastSkillPaths: string[];
|
||||
private extensionSkillSourceInfos: Map<string, SourceInfo>;
|
||||
private extensionPromptSourceInfos: Map<string, SourceInfo>;
|
||||
private extensionThemeSourceInfos: Map<string, SourceInfo>;
|
||||
private lastPromptPaths: string[];
|
||||
private lastThemePaths: string[];
|
||||
|
||||
@@ -236,8 +238,10 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.themeDiagnostics = [];
|
||||
this.agentsFiles = [];
|
||||
this.appendSystemPrompt = [];
|
||||
this.pathMetadata = new Map();
|
||||
this.lastSkillPaths = [];
|
||||
this.extensionSkillSourceInfos = new Map();
|
||||
this.extensionPromptSourceInfos = new Map();
|
||||
this.extensionThemeSourceInfos = new Map();
|
||||
this.lastPromptPaths = [];
|
||||
this.lastThemePaths = [];
|
||||
}
|
||||
@@ -270,21 +274,27 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
return this.appendSystemPrompt;
|
||||
}
|
||||
|
||||
getPathMetadata(): Map<string, PathMetadata> {
|
||||
return this.pathMetadata;
|
||||
}
|
||||
|
||||
extendResources(paths: ResourceExtensionPaths): void {
|
||||
const skillPaths = this.normalizeExtensionPaths(paths.skillPaths ?? []);
|
||||
const promptPaths = this.normalizeExtensionPaths(paths.promptPaths ?? []);
|
||||
const themePaths = this.normalizeExtensionPaths(paths.themePaths ?? []);
|
||||
|
||||
for (const entry of skillPaths) {
|
||||
this.extensionSkillSourceInfos.set(entry.path, createSourceInfo(entry.path, entry.metadata));
|
||||
}
|
||||
for (const entry of promptPaths) {
|
||||
this.extensionPromptSourceInfos.set(entry.path, createSourceInfo(entry.path, entry.metadata));
|
||||
}
|
||||
for (const entry of themePaths) {
|
||||
this.extensionThemeSourceInfos.set(entry.path, createSourceInfo(entry.path, entry.metadata));
|
||||
}
|
||||
|
||||
if (skillPaths.length > 0) {
|
||||
this.lastSkillPaths = this.mergePaths(
|
||||
this.lastSkillPaths,
|
||||
skillPaths.map((entry) => entry.path),
|
||||
);
|
||||
this.updateSkillsFromPaths(this.lastSkillPaths, skillPaths);
|
||||
this.updateSkillsFromPaths(this.lastSkillPaths);
|
||||
}
|
||||
|
||||
if (promptPaths.length > 0) {
|
||||
@@ -292,7 +302,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastPromptPaths,
|
||||
promptPaths.map((entry) => entry.path),
|
||||
);
|
||||
this.updatePromptsFromPaths(this.lastPromptPaths, promptPaths);
|
||||
this.updatePromptsFromPaths(this.lastPromptPaths);
|
||||
}
|
||||
|
||||
if (themePaths.length > 0) {
|
||||
@@ -300,7 +310,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
this.lastThemePaths,
|
||||
themePaths.map((entry) => entry.path),
|
||||
);
|
||||
this.updateThemesFromPaths(this.lastThemePaths, themePaths);
|
||||
this.updateThemesFromPaths(this.lastThemePaths);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,14 +319,19 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
const cliExtensionPaths = await this.packageManager.resolveExtensionSources(this.additionalExtensionPaths, {
|
||||
temporary: true,
|
||||
});
|
||||
const metadataByPath = new Map<string, PathMetadata>();
|
||||
|
||||
this.extensionSkillSourceInfos = new Map();
|
||||
this.extensionPromptSourceInfos = new Map();
|
||||
this.extensionThemeSourceInfos = new Map();
|
||||
|
||||
// Helper to extract enabled paths and store metadata
|
||||
const getEnabledResources = (
|
||||
resources: Array<{ path: string; enabled: boolean; metadata: PathMetadata }>,
|
||||
): Array<{ path: string; enabled: boolean; metadata: PathMetadata }> => {
|
||||
for (const r of resources) {
|
||||
if (!this.pathMetadata.has(r.path)) {
|
||||
this.pathMetadata.set(r.path, r.metadata);
|
||||
if (!metadataByPath.has(r.path)) {
|
||||
metadataByPath.set(r.path, r.metadata);
|
||||
}
|
||||
}
|
||||
return resources.filter((r) => r.enabled);
|
||||
@@ -325,9 +340,6 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
const getEnabledPaths = (
|
||||
resources: Array<{ path: string; enabled: boolean; metadata: PathMetadata }>,
|
||||
): string[] => getEnabledResources(resources).map((r) => r.path);
|
||||
|
||||
// Store metadata and get enabled paths
|
||||
this.pathMetadata = new Map();
|
||||
const enabledExtensions = getEnabledPaths(resolvedPaths.extensions);
|
||||
const enabledSkillResources = getEnabledResources(resolvedPaths.skills);
|
||||
const enabledPrompts = getEnabledPaths(resolvedPaths.prompts);
|
||||
@@ -347,8 +359,8 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
const skillFile = join(resource.path, "SKILL.md");
|
||||
if (existsSync(skillFile)) {
|
||||
if (!this.pathMetadata.has(skillFile)) {
|
||||
this.pathMetadata.set(skillFile, resource.metadata);
|
||||
if (!metadataByPath.has(skillFile)) {
|
||||
metadataByPath.set(skillFile, resource.metadata);
|
||||
}
|
||||
return skillFile;
|
||||
}
|
||||
@@ -359,13 +371,13 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
|
||||
// Add CLI paths metadata
|
||||
for (const r of cliExtensionPaths.extensions) {
|
||||
if (!this.pathMetadata.has(r.path)) {
|
||||
this.pathMetadata.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
if (!metadataByPath.has(r.path)) {
|
||||
metadataByPath.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
}
|
||||
}
|
||||
for (const r of cliExtensionPaths.skills) {
|
||||
if (!this.pathMetadata.has(r.path)) {
|
||||
this.pathMetadata.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
if (!metadataByPath.has(r.path)) {
|
||||
metadataByPath.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,31 +403,28 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
this.extensionsResult = this.extensionsOverride ? this.extensionsOverride(extensionsResult) : extensionsResult;
|
||||
this.applyExtensionSourceInfo(this.extensionsResult.extensions, metadataByPath);
|
||||
|
||||
const skillPaths = this.noSkills
|
||||
? this.mergePaths(cliEnabledSkills, this.additionalSkillPaths)
|
||||
: this.mergePaths([...enabledSkills, ...cliEnabledSkills], this.additionalSkillPaths);
|
||||
|
||||
this.lastSkillPaths = skillPaths;
|
||||
this.updateSkillsFromPaths(skillPaths);
|
||||
this.updateSkillsFromPaths(skillPaths, metadataByPath);
|
||||
|
||||
const promptPaths = this.noPromptTemplates
|
||||
? this.mergePaths(cliEnabledPrompts, this.additionalPromptTemplatePaths)
|
||||
: this.mergePaths([...enabledPrompts, ...cliEnabledPrompts], this.additionalPromptTemplatePaths);
|
||||
|
||||
this.lastPromptPaths = promptPaths;
|
||||
this.updatePromptsFromPaths(promptPaths);
|
||||
this.updatePromptsFromPaths(promptPaths, metadataByPath);
|
||||
|
||||
const themePaths = this.noThemes
|
||||
? this.mergePaths(cliEnabledThemes, this.additionalThemePaths)
|
||||
: this.mergePaths([...enabledThemes, ...cliEnabledThemes], this.additionalThemePaths);
|
||||
|
||||
this.lastThemePaths = themePaths;
|
||||
this.updateThemesFromPaths(themePaths);
|
||||
|
||||
for (const extension of this.extensionsResult.extensions) {
|
||||
this.addDefaultMetadataForPath(extension.path);
|
||||
}
|
||||
this.updateThemesFromPaths(themePaths, metadataByPath);
|
||||
|
||||
const agentsFiles = { agentsFiles: loadProjectContextFiles({ cwd: this.cwd, agentDir: this.agentDir }) };
|
||||
const resolvedAgentsFiles = this.agentsFilesOverride ? this.agentsFilesOverride(agentsFiles) : agentsFiles;
|
||||
@@ -444,10 +453,7 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
}));
|
||||
}
|
||||
|
||||
private updateSkillsFromPaths(
|
||||
skillPaths: string[],
|
||||
extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [],
|
||||
): void {
|
||||
private updateSkillsFromPaths(skillPaths: string[], metadataByPath?: Map<string, PathMetadata>): void {
|
||||
let skillsResult: { skills: Skill[]; diagnostics: ResourceDiagnostic[] };
|
||||
if (this.noSkills && skillPaths.length === 0) {
|
||||
skillsResult = { skills: [], diagnostics: [] };
|
||||
@@ -460,21 +466,16 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
});
|
||||
}
|
||||
const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult;
|
||||
this.skills = resolvedSkills.skills;
|
||||
this.skills = resolvedSkills.skills.map((skill) => ({
|
||||
...skill,
|
||||
sourceInfo:
|
||||
this.findSourceInfoForPath(skill.filePath, this.extensionSkillSourceInfos, metadataByPath) ??
|
||||
this.getDefaultSourceInfoForPath(skill.filePath),
|
||||
}));
|
||||
this.skillDiagnostics = resolvedSkills.diagnostics;
|
||||
this.applyExtensionMetadata(
|
||||
extensionPaths,
|
||||
this.skills.map((skill) => skill.filePath),
|
||||
);
|
||||
for (const skill of this.skills) {
|
||||
this.addDefaultMetadataForPath(skill.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private updatePromptsFromPaths(
|
||||
promptPaths: string[],
|
||||
extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [],
|
||||
): void {
|
||||
private updatePromptsFromPaths(promptPaths: string[], metadataByPath?: Map<string, PathMetadata>): void {
|
||||
let promptsResult: { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] };
|
||||
if (this.noPromptTemplates && promptPaths.length === 0) {
|
||||
promptsResult = { prompts: [], diagnostics: [] };
|
||||
@@ -488,21 +489,16 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
promptsResult = this.dedupePrompts(allPrompts);
|
||||
}
|
||||
const resolvedPrompts = this.promptsOverride ? this.promptsOverride(promptsResult) : promptsResult;
|
||||
this.prompts = resolvedPrompts.prompts;
|
||||
this.prompts = resolvedPrompts.prompts.map((prompt) => ({
|
||||
...prompt,
|
||||
sourceInfo:
|
||||
this.findSourceInfoForPath(prompt.filePath, this.extensionPromptSourceInfos, metadataByPath) ??
|
||||
this.getDefaultSourceInfoForPath(prompt.filePath),
|
||||
}));
|
||||
this.promptDiagnostics = resolvedPrompts.diagnostics;
|
||||
this.applyExtensionMetadata(
|
||||
extensionPaths,
|
||||
this.prompts.map((prompt) => prompt.filePath),
|
||||
);
|
||||
for (const prompt of this.prompts) {
|
||||
this.addDefaultMetadataForPath(prompt.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private updateThemesFromPaths(
|
||||
themePaths: string[],
|
||||
extensionPaths: Array<{ path: string; metadata: PathMetadata }> = [],
|
||||
): void {
|
||||
private updateThemesFromPaths(themePaths: string[], metadataByPath?: Map<string, PathMetadata>): void {
|
||||
let themesResult: { themes: Theme[]; diagnostics: ResourceDiagnostic[] };
|
||||
if (this.noThemes && themePaths.length === 0) {
|
||||
themesResult = { themes: [], diagnostics: [] };
|
||||
@@ -512,49 +508,115 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
themesResult = { themes: deduped.themes, diagnostics: [...loaded.diagnostics, ...deduped.diagnostics] };
|
||||
}
|
||||
const resolvedThemes = this.themesOverride ? this.themesOverride(themesResult) : themesResult;
|
||||
this.themes = resolvedThemes.themes;
|
||||
this.themes = resolvedThemes.themes.map((theme) => {
|
||||
const sourcePath = theme.sourcePath;
|
||||
theme.sourceInfo = sourcePath
|
||||
? (this.findSourceInfoForPath(sourcePath, this.extensionThemeSourceInfos, metadataByPath) ??
|
||||
this.getDefaultSourceInfoForPath(sourcePath))
|
||||
: undefined;
|
||||
return theme;
|
||||
});
|
||||
this.themeDiagnostics = resolvedThemes.diagnostics;
|
||||
const themePathsWithSource = this.themes.flatMap((theme) => (theme.sourcePath ? [theme.sourcePath] : []));
|
||||
this.applyExtensionMetadata(extensionPaths, themePathsWithSource);
|
||||
for (const theme of this.themes) {
|
||||
if (theme.sourcePath) {
|
||||
this.addDefaultMetadataForPath(theme.sourcePath);
|
||||
}
|
||||
|
||||
private applyExtensionSourceInfo(extensions: Extension[], metadataByPath: Map<string, PathMetadata>): void {
|
||||
for (const extension of extensions) {
|
||||
extension.sourceInfo =
|
||||
this.findSourceInfoForPath(extension.path, undefined, metadataByPath) ??
|
||||
this.getDefaultSourceInfoForPath(extension.path);
|
||||
for (const command of extension.commands.values()) {
|
||||
command.sourceInfo = extension.sourceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private applyExtensionMetadata(
|
||||
extensionPaths: Array<{ path: string; metadata: PathMetadata }>,
|
||||
resourcePaths: string[],
|
||||
): void {
|
||||
if (extensionPaths.length === 0) {
|
||||
return;
|
||||
private findSourceInfoForPath(
|
||||
resourcePath: string,
|
||||
extraSourceInfos?: Map<string, SourceInfo>,
|
||||
metadataByPath?: Map<string, PathMetadata>,
|
||||
): SourceInfo | undefined {
|
||||
if (!resourcePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = extensionPaths.map((entry) => ({
|
||||
path: resolve(entry.path),
|
||||
metadata: entry.metadata,
|
||||
}));
|
||||
if (resourcePath.startsWith("<")) {
|
||||
return this.getDefaultSourceInfoForPath(resourcePath);
|
||||
}
|
||||
|
||||
for (const entry of normalized) {
|
||||
if (!this.pathMetadata.has(entry.path)) {
|
||||
this.pathMetadata.set(entry.path, entry.metadata);
|
||||
const normalizedResourcePath = resolve(resourcePath);
|
||||
if (extraSourceInfos) {
|
||||
for (const [sourcePath, sourceInfo] of extraSourceInfos.entries()) {
|
||||
const normalizedSourcePath = resolve(sourcePath);
|
||||
if (
|
||||
normalizedResourcePath === normalizedSourcePath ||
|
||||
normalizedResourcePath.startsWith(`${normalizedSourcePath}${sep}`)
|
||||
) {
|
||||
return { ...sourceInfo, path: resourcePath };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const resourcePath of resourcePaths) {
|
||||
const normalizedResourcePath = resolve(resourcePath);
|
||||
if (this.pathMetadata.has(normalizedResourcePath) || this.pathMetadata.has(resourcePath)) {
|
||||
continue;
|
||||
if (metadataByPath) {
|
||||
const exact = metadataByPath.get(normalizedResourcePath) ?? metadataByPath.get(resourcePath);
|
||||
if (exact) {
|
||||
return createSourceInfo(resourcePath, exact);
|
||||
}
|
||||
const match = normalized.find(
|
||||
(entry) =>
|
||||
normalizedResourcePath === entry.path || normalizedResourcePath.startsWith(`${entry.path}${sep}`),
|
||||
);
|
||||
if (match) {
|
||||
this.pathMetadata.set(normalizedResourcePath, match.metadata);
|
||||
|
||||
for (const [sourcePath, metadata] of metadataByPath.entries()) {
|
||||
const normalizedSourcePath = resolve(sourcePath);
|
||||
if (
|
||||
normalizedResourcePath === normalizedSourcePath ||
|
||||
normalizedResourcePath.startsWith(`${normalizedSourcePath}${sep}`)
|
||||
) {
|
||||
return createSourceInfo(resourcePath, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getDefaultSourceInfoForPath(filePath: string): SourceInfo | undefined {
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (filePath.startsWith("<") && filePath.endsWith(">")) {
|
||||
return {
|
||||
path: filePath,
|
||||
source: filePath.slice(1, -1).split(":")[0] || "temporary",
|
||||
scope: "temporary",
|
||||
origin: "top-level",
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedPath = resolve(filePath);
|
||||
const agentRoots = [
|
||||
join(this.agentDir, "skills"),
|
||||
join(this.agentDir, "prompts"),
|
||||
join(this.agentDir, "themes"),
|
||||
join(this.agentDir, "extensions"),
|
||||
];
|
||||
const projectRoots = [
|
||||
join(this.cwd, CONFIG_DIR_NAME, "skills"),
|
||||
join(this.cwd, CONFIG_DIR_NAME, "prompts"),
|
||||
join(this.cwd, CONFIG_DIR_NAME, "themes"),
|
||||
join(this.cwd, CONFIG_DIR_NAME, "extensions"),
|
||||
];
|
||||
|
||||
for (const root of agentRoots) {
|
||||
if (this.isUnderPath(normalizedPath, root)) {
|
||||
return { path: filePath, source: "local", scope: "user", origin: "top-level" };
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of projectRoots) {
|
||||
if (this.isUnderPath(normalizedPath, root)) {
|
||||
return { path: filePath, source: "local", scope: "project", origin: "top-level" };
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private mergePaths(primary: string[], additional: string[]): string[] {
|
||||
@@ -767,44 +829,6 @@ export class DefaultResourceLoader implements ResourceLoader {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private addDefaultMetadataForPath(filePath: string): void {
|
||||
if (!filePath || filePath.startsWith("<")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedPath = resolve(filePath);
|
||||
if (this.pathMetadata.has(normalizedPath) || this.pathMetadata.has(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const agentRoots = [
|
||||
join(this.agentDir, "skills"),
|
||||
join(this.agentDir, "prompts"),
|
||||
join(this.agentDir, "themes"),
|
||||
join(this.agentDir, "extensions"),
|
||||
];
|
||||
const projectRoots = [
|
||||
join(this.cwd, CONFIG_DIR_NAME, "skills"),
|
||||
join(this.cwd, CONFIG_DIR_NAME, "prompts"),
|
||||
join(this.cwd, CONFIG_DIR_NAME, "themes"),
|
||||
join(this.cwd, CONFIG_DIR_NAME, "extensions"),
|
||||
];
|
||||
|
||||
for (const root of agentRoots) {
|
||||
if (this.isUnderPath(normalizedPath, root)) {
|
||||
this.pathMetadata.set(normalizedPath, { source: "local", scope: "user", origin: "top-level" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of projectRoots) {
|
||||
if (this.isUnderPath(normalizedPath, root)) {
|
||||
this.pathMetadata.set(normalizedPath, { source: "local", scope: "project", origin: "top-level" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isUnderPath(target: string, root: string): boolean {
|
||||
const normalizedRoot = resolve(root);
|
||||
if (target === normalizedRoot) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "pat
|
||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
import type { SourceInfo } from "./source-info.js";
|
||||
|
||||
/** Max name length per spec */
|
||||
const MAX_NAME_LENGTH = 64;
|
||||
@@ -76,6 +77,7 @@ export interface Skill {
|
||||
filePath: string;
|
||||
baseDir: string;
|
||||
source: string;
|
||||
sourceInfo?: SourceInfo;
|
||||
disableModelInvocation: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { SourceInfo } from "./source-info.js";
|
||||
|
||||
export type SlashCommandSource = "extension" | "prompt" | "skill";
|
||||
|
||||
export type SlashCommandLocation = "user" | "project" | "path";
|
||||
@@ -6,6 +8,7 @@ export interface SlashCommandInfo {
|
||||
name: string;
|
||||
description?: string;
|
||||
source: SlashCommandSource;
|
||||
sourceInfo?: SourceInfo;
|
||||
location?: SlashCommandLocation;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
19
packages/coding-agent/src/core/source-info.ts
Normal file
19
packages/coding-agent/src/core/source-info.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { PathMetadata } from "./package-manager.js";
|
||||
|
||||
export interface SourceInfo {
|
||||
path?: string;
|
||||
source: string;
|
||||
scope: "user" | "project" | "temporary";
|
||||
origin: "package" | "top-level";
|
||||
baseDir?: string;
|
||||
}
|
||||
|
||||
export function createSourceInfo(path: string | undefined, metadata: PathMetadata): SourceInfo {
|
||||
return {
|
||||
path,
|
||||
source: metadata.source,
|
||||
scope: metadata.scope,
|
||||
origin: metadata.origin,
|
||||
baseDir: metadata.baseDir,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user