pi-code 1.0.22 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,7 +52,7 @@ import { capForContext } from './internal/output-guard.js'
52
52
  import { matchesPathRules } from './internal/path-rules.js'
53
53
  import { type InstalledPlugin, installedPlugins } from './internal/plugins.js'
54
54
  import { isProjectApproved } from './internal/project-approval.js'
55
- import { findNearestDir, repoRoot } from './internal/project-root.js'
55
+ import { ancestorDirs, repoRoot } from './internal/project-root.js'
56
56
  import { claudeSettingsChain } from './internal/settings-chain.js'
57
57
  import { createTurnOverride } from './internal/turn-override.js'
58
58
 
@@ -120,7 +120,10 @@ function isDirectory(target: string): boolean {
120
120
  * the approval walk) and is included only for approved projects. */
121
121
  export function commandDirs(cwd: string, home: string, trusted: boolean): string[] {
122
122
  const candidates = [path.join(claudeConfigDir(home), 'commands')]
123
- if (trusted) candidates.push(findNearestDir(cwd, path.join('.claude', 'commands')) ?? path.join(cwd, '.claude', 'commands'))
123
+ // Claude scans every .claude/commands between cwd and the repository root, the
124
+ // nearest winning a name clash: collectCommands lets later directories win, so
125
+ // the project list goes root-first with the nearest last.
126
+ if (trusted) candidates.push(...ancestorDirs(cwd, path.join('.claude', 'commands')).reverse())
124
127
  const dirs: string[] = []
125
128
  for (const dir of candidates) {
126
129
  if (!dirs.includes(dir) && isDirectory(dir)) dirs.push(dir)
@@ -60,6 +60,24 @@ export function findNearestFile(cwd: string, relative: string): string | null {
60
60
  return findNearest(cwd, relative, false)
61
61
  }
62
62
 
63
+ /** Every `relative` directory between cwd and the repository root, nearest first,
64
+ * matching Claude's "every .claude/<kind> between the working directory and the
65
+ * repository root" discovery where the entry closest to cwd wins a name clash. */
66
+ export function ancestorDirs(cwd: string, relative: string): string[] {
67
+ const boundary = repoRoot(cwd) ?? cwd
68
+ const found: string[] = []
69
+ let currentDir = cwd
70
+ while (true) {
71
+ const candidate = path.join(currentDir, relative)
72
+ if (statOf(candidate)?.isDirectory()) found.push(candidate)
73
+ if (currentDir === boundary) break
74
+ const parentDir = path.dirname(currentDir)
75
+ if (parentDir === currentDir) break
76
+ currentDir = parentDir
77
+ }
78
+ return found
79
+ }
80
+
63
81
  /** Every `relative` file between the repository root and cwd, ordered root first,
64
82
  * matching Claude's root-down ordering for hierarchy-loaded context. */
65
83
  export function ancestorFiles(cwd: string, relative: string): string[] {
@@ -27,7 +27,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
27
27
  import { claudeConfigDir } from './internal/config-dir.js'
28
28
  import { installedPlugins } from './internal/plugins.js'
29
29
  import { isProjectApproved } from './internal/project-approval.js'
30
- import { findNearestDir, findNearestFile } from './internal/project-root.js'
30
+ import { ancestorDirs, findNearestDir, findNearestFile } from './internal/project-root.js'
31
31
  import { claudeSettingsChain } from './internal/settings-chain.js'
32
32
 
33
33
  export interface OutputStyle {
@@ -85,7 +85,10 @@ function isDirectory(target: string): boolean {
85
85
  */
86
86
  export function styleDirs(cwd: string, home: string, trusted: boolean): string[] {
87
87
  const dirs = [path.join(claudeConfigDir(home), 'output-styles')]
88
- if (trusted) dirs.push(findNearestDir(cwd, path.join('.claude', 'output-styles')) ?? path.join(cwd, '.claude', 'output-styles'))
88
+ // Claude loads every .claude/output-styles between cwd and the repository root,
89
+ // using the one closest to cwd for a name clash: styleForName is first-match,
90
+ // so the nearest directory goes first.
91
+ if (trusted) dirs.push(...ancestorDirs(cwd, path.join('.claude', 'output-styles')))
89
92
  return dirs.filter((dir) => isDirectory(dir))
90
93
  }
91
94
 
@@ -29,7 +29,7 @@ import { parseCommandFile } from './internal/command-file.js'
29
29
  import { claudeConfigDir } from './internal/config-dir.js'
30
30
  import { installedPlugins } from './internal/plugins.js'
31
31
  import { isProjectApprovedSilently } from './internal/project-approval.js'
32
- import { findNearestDir } from './internal/project-root.js'
32
+ import { ancestorDirs } from './internal/project-root.js'
33
33
 
34
34
  function isDirectory(target: string): boolean {
35
35
  try {
@@ -53,7 +53,10 @@ export function skillDirs(cwd: string, home: string, trusted: boolean): string[]
53
53
  const dirs = Array.isArray(declared) ? declared : [typeof declared === 'string' ? declared : 'skills']
54
54
  candidates.push(...dirs.map((dir) => path.resolve(plugin.root, String(dir))))
55
55
  }
56
- if (trusted) candidates.push(findNearestDir(cwd, path.join('.claude', 'skills')) ?? path.join(cwd, '.claude', 'skills'))
56
+ // Claude loads skills from every .claude/skills between cwd and the repository
57
+ // root; the list goes nearest-first so findClaudeSkill's first match is the
58
+ // closest definition (pi's loader receives the same order).
59
+ if (trusted) candidates.push(...ancestorDirs(cwd, path.join('.claude', 'skills')))
57
60
  const dirs: string[] = []
58
61
  for (const dir of candidates) {
59
62
  if (!dirs.includes(dir) && isDirectory(dir)) dirs.push(dir)
@@ -13,7 +13,7 @@ import { getAgentDir, parseFrontmatter, stripFrontmatter } from '@earendil-works
13
13
  import { parseToolGrants } from '../internal/command-file.js'
14
14
  import { claudeConfigDir } from '../internal/config-dir.js'
15
15
  import { installedPlugins } from '../internal/plugins.js'
16
- import { findNearestDir } from '../internal/project-root.js'
16
+ import { ancestorDirs, findNearestDir } from '../internal/project-root.js'
17
17
 
18
18
  /**
19
19
  * `tools:` may be a comma-separated string (the Claude Code format) or a YAML block
@@ -337,13 +337,16 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
337
337
  const userDir = path.join(getAgentDir(), 'agents')
338
338
  const claudeUserDir = path.join(claudeConfigDir(os.homedir()), 'agents')
339
339
  const projectPiDir = findNearestDir(cwd, path.join('.pi', 'agents'))
340
- const projectClaudeDir = findNearestDir(cwd, path.join('.claude', 'agents'))
340
+ // Claude scans every .claude/agents between cwd and the repository root, the
341
+ // definition closest to cwd winning a name clash; root-first load order makes
342
+ // the nearer directory's entry overwrite in the map below.
343
+ const projectClaudeDirs = ancestorDirs(cwd, path.join('.claude', 'agents')).reverse()
341
344
 
342
345
  // Plugins load after builtins and before the user's own dirs, so a user agent
343
346
  // wins a name clash with a plugin's, and ~/.pi/agent/agents wins over ~/.claude.
344
347
  const userAgents = scope === 'project' ? [] : [...loadAgentsFromDir(BUILTIN_AGENTS_DIR, 'builtin'), ...pluginAgentDirs(os.homedir()).flatMap((dir) => loadAgentsFromDir(dir, 'plugin')), ...loadAgentsFromDir(claudeUserDir, 'user'), ...loadAgentsFromDir(userDir, 'user')]
345
348
  // project .claude/agents loads first so project .pi/agents wins on name conflicts
346
- const projectAgents = scope === 'user' ? [] : [...(projectClaudeDir ? loadAgentsFromDir(projectClaudeDir, 'project') : []), ...(projectPiDir ? loadAgentsFromDir(projectPiDir, 'project') : [])]
349
+ const projectAgents = scope === 'user' ? [] : [...projectClaudeDirs.flatMap((dir) => loadAgentsFromDir(dir, 'project')), ...(projectPiDir ? loadAgentsFromDir(projectPiDir, 'project') : [])]
347
350
 
348
351
  const agentMap = buildAgentMap(userAgents, projectAgents, scope)
349
352
  return { agents: Array.from(agentMap.values()), projectAgentsDir: projectPiDir }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-code",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
5
5
  "keywords": [
6
6
  "pi",