Skip to content

Commit 2b85f24

Browse files
committed
feat(imports): add import-aware path resolution utilities
Implement new resolution functions that check imported packages before local paths. This enables better sharing of resources across projects while maintaining backwards compatibility. The changes include: - New resolve.js module with path resolution utilities - Updates to prompt, workflow and agent loading to use imports first - Clearer error messages when resources aren't found - Improved debug logging for resolution paths
1 parent 44e864b commit 2b85f24

12 files changed

Lines changed: 540 additions & 89 deletions

File tree

src/agents/runner/chained.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import * as path from 'node:path';
33

44
import { debug } from '../../shared/logging/logger.js';
55
import type { ChainedPathEntry, ConditionalChainedPath } from '../../shared/agents/config/types.js';
6+
import { resolvePathWithImports } from '../../shared/imports/index.js';
7+
import { resolvePackageRoot } from '../../shared/runtime/root.js';
8+
9+
const packageRoot = resolvePackageRoot(import.meta.url, 'chained prompts');
610

711
/**
812
* Represents a chained prompt loaded from a .md file
@@ -89,14 +93,20 @@ function filenameToName(filename: string): string {
8993

9094
/**
9195
* Load a single chained prompt from a file
96+
* Checks imported packages first, then project root
9297
*/
9398
async function loadPromptFromFile(
9499
filePath: string,
95100
projectRoot: string
96101
): Promise<ChainedPrompt | null> {
97-
const absolutePath = path.isAbsolute(filePath)
98-
? filePath
99-
: path.resolve(projectRoot, filePath);
102+
let absolutePath: string;
103+
if (path.isAbsolute(filePath)) {
104+
absolutePath = filePath;
105+
} else {
106+
// Try to resolve from imports first, then fall back to project root
107+
const importResolved = resolvePathWithImports(filePath, packageRoot, [projectRoot]);
108+
absolutePath = importResolved ?? path.resolve(projectRoot, filePath);
109+
}
100110

101111
try {
102112
const rawContent = await fs.readFile(absolutePath, 'utf-8');
@@ -117,15 +127,20 @@ async function loadPromptFromFile(
117127

118128
/**
119129
* Load chained prompts from a single folder
130+
* Checks imported packages first, then project root
120131
*/
121132
async function loadPromptsFromFolder(
122133
folderPath: string,
123134
projectRoot: string
124135
): Promise<ChainedPrompt[]> {
125-
// Resolve path
126-
const absolutePath = path.isAbsolute(folderPath)
127-
? folderPath
128-
: path.resolve(projectRoot, folderPath);
136+
// Resolve path - check imports first
137+
let absolutePath: string;
138+
if (path.isAbsolute(folderPath)) {
139+
absolutePath = folderPath;
140+
} else {
141+
const importResolved = resolvePathWithImports(folderPath, packageRoot, [projectRoot]);
142+
absolutePath = importResolved ?? path.resolve(projectRoot, folderPath);
143+
}
129144

130145
// Check if directory exists
131146
try {
@@ -174,14 +189,19 @@ async function loadPromptsFromFolder(
174189

175190
/**
176191
* Load chained prompts from a path (file or folder)
192+
* Checks imported packages first, then project root
177193
*/
178194
async function loadPromptsFromPath(
179195
inputPath: string,
180196
projectRoot: string
181197
): Promise<ChainedPrompt[]> {
182-
const absolutePath = path.isAbsolute(inputPath)
183-
? inputPath
184-
: path.resolve(projectRoot, inputPath);
198+
let absolutePath: string;
199+
if (path.isAbsolute(inputPath)) {
200+
absolutePath = inputPath;
201+
} else {
202+
const importResolved = resolvePathWithImports(inputPath, packageRoot, [projectRoot]);
203+
absolutePath = importResolved ?? path.resolve(projectRoot, inputPath);
204+
}
185205

186206
try {
187207
const stat = await fs.stat(absolutePath);

src/agents/runner/config.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import * as path from 'node:path';
33

44
import { collectAgentDefinitions, resolveProjectRoot } from '../../shared/agents/index.js';
55
import type { AgentDefinition } from '../../shared/agents/config/types.js';
6+
import { resolvePromptPath } from '../../shared/imports/index.js';
7+
import { resolvePackageRoot } from '../../shared/runtime/root.js';
8+
9+
const packageRoot = resolvePackageRoot(import.meta.url, 'agent runner config');
610

711
export type AgentConfig = AgentDefinition & {
812
name: string;
@@ -65,10 +69,17 @@ export async function loadAgentTemplate(agentId: string, projectRoot?: string):
6569
throw new Error(`Agent ${agentId} has an invalid promptPath configuration`);
6670
}
6771

68-
// If path is absolute, use it directly; otherwise resolve relative to project root
69-
const resolvedPromptPaths = promptSources.map(p =>
70-
path.isAbsolute(p) ? p : path.resolve(resolvedRoot, p),
71-
);
72+
// If path is absolute, use it directly; otherwise check imports first, then project root
73+
const resolvedPromptPaths = promptSources.map(p => {
74+
if (path.isAbsolute(p)) return p;
75+
76+
// Try to resolve from imports first
77+
const importResolved = resolvePromptPath(p, packageRoot);
78+
if (importResolved) return importResolved;
79+
80+
// Fall back to project root
81+
return path.resolve(resolvedRoot, p);
82+
});
7283

7384
const contentParts = await Promise.all(
7485
resolvedPromptPaths.map(promptPath => fs.readFile(promptPath, 'utf-8')),

src/shared/agents/config/paths.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
import { existsSync } from 'node:fs';
22
import { join, resolve } from 'node:path';
3+
import { getAllInstalledImports } from '../../imports/index.js';
34

45
const AGENT_MODULE_FILENAMES = ['sub.agents.js', 'main.agents.js', 'modules.js', 'agents.js'];
56
const AGENT_JSON_RELATIVE_PATH = join('.codemachine', 'agents', 'agents-config.json');
67

78
export type AgentsModuleLookupOptions = {
89
projectRoot?: string;
10+
/** Whether to also check imported packages */
11+
checkImports?: boolean;
912
};
1013

1114
export function resolveAgentsModulePath(options: AgentsModuleLookupOptions = {}): string | undefined {
1215
const projectRoot = options.projectRoot ? resolve(options.projectRoot) : undefined;
13-
if (!projectRoot) {
14-
return undefined;
16+
const checkImports = options.checkImports ?? true;
17+
18+
const candidates: string[] = [];
19+
20+
// Check imported packages first (they take precedence)
21+
if (checkImports) {
22+
const imports = getAllInstalledImports();
23+
for (const imp of imports) {
24+
for (const filename of AGENT_MODULE_FILENAMES) {
25+
candidates.push(join(imp.resolvedPaths.config, filename));
26+
}
27+
}
1528
}
1629

17-
const candidates = [join(projectRoot, AGENT_JSON_RELATIVE_PATH)];
30+
// Then check project root
31+
if (projectRoot) {
32+
candidates.push(join(projectRoot, AGENT_JSON_RELATIVE_PATH));
1833

19-
for (const filename of AGENT_MODULE_FILENAMES) {
20-
candidates.push(join(projectRoot, 'config', filename));
21-
candidates.push(join(projectRoot, 'dist', 'config', filename));
34+
for (const filename of AGENT_MODULE_FILENAMES) {
35+
candidates.push(join(projectRoot, 'config', filename));
36+
candidates.push(join(projectRoot, 'dist', 'config', filename));
37+
}
2238
}
2339

2440
for (const candidate of candidates) {
@@ -29,3 +45,44 @@ export function resolveAgentsModulePath(options: AgentsModuleLookupOptions = {})
2945

3046
return undefined;
3147
}
48+
49+
/**
50+
* Get all agent module paths including from imports
51+
* Returns an array of all existing agent config paths
52+
*/
53+
export function getAllAgentsModulePaths(projectRoot?: string): string[] {
54+
const paths: string[] = [];
55+
const resolvedRoot = projectRoot ? resolve(projectRoot) : undefined;
56+
57+
// Check imported packages first
58+
const imports = getAllInstalledImports();
59+
for (const imp of imports) {
60+
for (const filename of AGENT_MODULE_FILENAMES) {
61+
const candidate = join(imp.resolvedPaths.config, filename);
62+
if (existsSync(candidate)) {
63+
paths.push(candidate);
64+
}
65+
}
66+
}
67+
68+
// Then check project root
69+
if (resolvedRoot) {
70+
const jsonPath = join(resolvedRoot, AGENT_JSON_RELATIVE_PATH);
71+
if (existsSync(jsonPath)) {
72+
paths.push(jsonPath);
73+
}
74+
75+
for (const filename of AGENT_MODULE_FILENAMES) {
76+
const configPath = join(resolvedRoot, 'config', filename);
77+
if (existsSync(configPath)) {
78+
paths.push(configPath);
79+
}
80+
const distPath = join(resolvedRoot, 'dist', 'config', filename);
81+
if (existsSync(distPath)) {
82+
paths.push(distPath);
83+
}
84+
}
85+
}
86+
87+
return paths;
88+
}

src/shared/agents/discovery/steps.ts

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { existsSync, readdirSync } from 'node:fs';
22
import * as path from 'node:path';
33

44
import { loadWorkflowModule, isWorkflowTemplate } from '../../../workflows/index.js';
5+
import { getAllWorkflowDirectories } from '../../imports/index.js';
56

67
export type WorkflowAgentDefinition = {
78
id: string;
@@ -22,46 +23,85 @@ function discoverWorkflowFiles(root: string): string[] {
2223
.map((file) => path.resolve(baseDir, file));
2324
}
2425

26+
/**
27+
* Discover workflow files from all workflow directories including imports
28+
*/
29+
function discoverAllWorkflowFiles(localRoot: string): string[] {
30+
const allFiles: string[] = [];
31+
const seenDirs = new Set<string>();
32+
33+
// Get all workflow directories (imports + local)
34+
const workflowDirs = getAllWorkflowDirectories(localRoot);
35+
36+
for (const dir of workflowDirs) {
37+
if (seenDirs.has(dir) || !existsSync(dir)) continue;
38+
seenDirs.add(dir);
39+
40+
const files = readdirSync(dir)
41+
.filter((file) => file.endsWith('.workflow.js'))
42+
.map((file) => path.resolve(dir, file));
43+
allFiles.push(...files);
44+
}
45+
46+
return allFiles;
47+
}
48+
2549
export async function collectAgentsFromWorkflows(roots: string[]): Promise<WorkflowAgentDefinition[]> {
2650
const seenFiles = new Set<string>();
2751
const byId = new Map<string, WorkflowAgentDefinition>();
2852

53+
// Collect workflow files from all roots
54+
const allWorkflowFiles: string[] = [];
55+
2956
for (const root of roots) {
3057
if (!root) continue;
3158
const resolvedRoot = path.resolve(root);
59+
60+
// Use both traditional discovery and import-aware discovery
3261
for (const filePath of discoverWorkflowFiles(resolvedRoot)) {
33-
if (seenFiles.has(filePath)) continue;
34-
seenFiles.add(filePath);
62+
allWorkflowFiles.push(filePath);
63+
}
64+
}
65+
66+
// Also discover from all import directories (handles case where imports aren't in roots)
67+
if (roots.length > 0) {
68+
const importFiles = discoverAllWorkflowFiles(roots[0]);
69+
allWorkflowFiles.push(...importFiles);
70+
}
71+
72+
// Process all discovered workflow files
73+
for (const filePath of allWorkflowFiles) {
74+
if (seenFiles.has(filePath)) continue;
75+
seenFiles.add(filePath);
3576

36-
try {
37-
const template = await loadWorkflowModule(filePath);
38-
if (!isWorkflowTemplate(template)) {
77+
try {
78+
const template = await loadWorkflowModule(filePath);
79+
if (!isWorkflowTemplate(template)) {
80+
continue;
81+
}
82+
83+
for (const step of template.steps ?? []) {
84+
if (!step || step.type !== 'module') {
3985
continue;
4086
}
4187

42-
for (const step of template.steps ?? []) {
43-
if (!step || step.type !== 'module') {
44-
continue;
45-
}
46-
47-
const id = typeof step.agentId === 'string' ? step.agentId.trim() : '';
48-
if (!id) {
49-
continue;
50-
}
51-
52-
const existing = byId.get(id) ?? { id };
53-
byId.set(id, {
54-
...existing,
55-
id,
56-
name: step.agentName ?? existing.name,
57-
promptPath: step.promptPath ?? existing.promptPath,
58-
model: step.model ?? existing.model,
59-
modelReasoningEffort: step.modelReasoningEffort ?? existing.modelReasoningEffort,
60-
});
88+
const id = typeof step.agentId === 'string' ? step.agentId.trim() : '';
89+
if (!id) {
90+
continue;
6191
}
62-
} catch {
63-
// Ignore templates that fail to load; other files might still provide definitions.
92+
93+
const existing = byId.get(id) ?? { id };
94+
byId.set(id, {
95+
...existing,
96+
id,
97+
name: step.agentName ?? existing.name,
98+
promptPath: step.promptPath ?? existing.promptPath,
99+
model: step.model ?? existing.model,
100+
modelReasoningEffort: step.modelReasoningEffort ?? existing.modelReasoningEffort,
101+
});
64102
}
103+
} catch {
104+
// Ignore templates that fail to load; other files might still provide definitions.
65105
}
66106
}
67107

src/shared/imports/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,13 @@ export {
5151
resolveSource,
5252
extractRepoName,
5353
} from './resolver.js';
54+
55+
// Import-aware path resolution
56+
export {
57+
resolvePromptPath,
58+
resolvePromptFolder,
59+
resolveWorkflowTemplate,
60+
resolvePathWithImports,
61+
getAllWorkflowDirectories,
62+
getAllPromptDirectories,
63+
} from './resolve.js';

0 commit comments

Comments
 (0)