@phi-code-admin/phi-code 0.85.0 → 0.86.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +92 -123
  3. package/config/routing.example.json +129 -0
  4. package/config/routing.schema.json +58 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +1 -1
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/core/sdk.d.ts.map +1 -1
  12. package/dist/core/sdk.js +1 -1
  13. package/dist/core/sdk.js.map +1 -1
  14. package/dist/core/system-prompt.d.ts.map +1 -1
  15. package/dist/core/system-prompt.js +6 -6
  16. package/dist/core/system-prompt.js.map +1 -1
  17. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/config-selector.js +1 -1
  19. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  21. package/dist/modes/interactive/interactive-mode.js +2 -2
  22. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  23. package/dist/package-manager-cli.d.ts.map +1 -1
  24. package/dist/package-manager-cli.js +9 -8
  25. package/dist/package-manager-cli.js.map +1 -1
  26. package/dist/utils/pi-user-agent.d.ts.map +1 -1
  27. package/dist/utils/pi-user-agent.js +4 -1
  28. package/dist/utils/pi-user-agent.js.map +1 -1
  29. package/docs/compaction.md +11 -11
  30. package/docs/custom-provider.md +4 -4
  31. package/docs/development.md +2 -2
  32. package/docs/extensions.md +47 -47
  33. package/docs/fork-policy.md +63 -0
  34. package/docs/index.md +7 -7
  35. package/docs/json.md +3 -3
  36. package/docs/keybindings.md +5 -5
  37. package/docs/models.md +6 -6
  38. package/docs/packages.md +37 -37
  39. package/docs/prompt-templates.md +4 -4
  40. package/docs/providers.md +9 -9
  41. package/docs/quickstart.md +25 -25
  42. package/docs/rpc.md +3 -3
  43. package/docs/sdk.md +34 -34
  44. package/docs/session-format.md +4 -4
  45. package/docs/sessions.md +11 -11
  46. package/docs/settings.md +9 -9
  47. package/docs/shell-aliases.md +2 -2
  48. package/docs/skills.md +9 -9
  49. package/docs/terminal-setup.md +7 -7
  50. package/docs/termux.md +6 -6
  51. package/docs/themes.md +9 -9
  52. package/docs/tmux.md +3 -3
  53. package/docs/tui.md +8 -8
  54. package/docs/usage.md +39 -39
  55. package/docs/windows.md +2 -2
  56. package/extensions/phi/agents.ts +5 -113
  57. package/extensions/phi/orchestrator.ts +6 -38
  58. package/extensions/phi/providers/agent-def.ts +128 -0
  59. package/extensions/phi/skill-loader.ts +5 -5
  60. package/package.json +2 -1
package/docs/tui.md CHANGED
@@ -1,4 +1,4 @@
1
- > pi can create TUI components. Ask it to build one for your use case.
1
+ > phi can create TUI components. Ask it to build one for your use case.
2
2
 
3
3
  # TUI Components
4
4
 
@@ -425,7 +425,7 @@ renderResult(result, options, theme, context) {
425
425
  **For Markdown**, use `getMarkdownTheme()`:
426
426
 
427
427
  ```typescript
428
- import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
428
+ import { getMarkdownTheme } from "@phi-code-admin/phi-code";
429
429
  import { Markdown } from "@earendil-works/pi-tui";
430
430
 
431
431
  renderResult(result, options, theme, context) {
@@ -590,8 +590,8 @@ These patterns cover the most common UI needs in extensions. **Copy these patter
590
590
  For letting users pick from a list of options. Use `SelectList` from `@earendil-works/pi-tui` with `DynamicBorder` for framing.
591
591
 
592
592
  ```typescript
593
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
594
- import { DynamicBorder } from "@earendil-works/pi-coding-agent";
593
+ import type { ExtensionAPI } from "@phi-code-admin/phi-code";
594
+ import { DynamicBorder } from "@phi-code-admin/phi-code";
595
595
  import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
596
596
 
597
597
  pi.registerCommand("pick", {
@@ -650,7 +650,7 @@ pi.registerCommand("pick", {
650
650
  For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
651
651
 
652
652
  ```typescript
653
- import { BorderedLoader } from "@earendil-works/pi-coding-agent";
653
+ import { BorderedLoader } from "@phi-code-admin/phi-code";
654
654
 
655
655
  pi.registerCommand("fetch", {
656
656
  handler: async (_args, ctx) => {
@@ -682,7 +682,7 @@ pi.registerCommand("fetch", {
682
682
  For toggling multiple settings. Use `SettingsList` from `@earendil-works/pi-tui` with `getSettingsListTheme()`.
683
683
 
684
684
  ```typescript
685
- import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
685
+ import { getSettingsListTheme } from "@phi-code-admin/phi-code";
686
686
  import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
687
687
 
688
688
  pi.registerCommand("settings", {
@@ -737,7 +737,7 @@ ctx.ui.setStatus("my-ext", undefined);
737
737
 
738
738
  ### Pattern 4b: Working Indicator Customization
739
739
 
740
- Customize the inline working indicator shown while pi is streaming a response.
740
+ Customize the inline working indicator shown while phi is streaming a response.
741
741
 
742
742
  ```typescript
743
743
  // Static indicator
@@ -822,7 +822,7 @@ Token stats available via `ctx.sessionManager.getBranch()` and `ctx.model`.
822
822
  Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
823
823
 
824
824
  ```typescript
825
- import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
825
+ import { CustomEditor, type ExtensionAPI } from "@phi-code-admin/phi-code";
826
826
  import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
827
827
 
828
828
  type Mode = "normal" | "insert";
package/docs/usage.md CHANGED
@@ -1,4 +1,4 @@
1
- # Using Pi
1
+ # Using phi
2
2
 
3
3
  This page collects day-to-day usage details that do not fit on the quickstart page.
4
4
 
@@ -53,7 +53,7 @@ Type `/` in the editor to open command completion. Extensions can register custo
53
53
  | `/reload` | Reload keybindings, extensions, skills, prompts, and context files |
54
54
  | `/hotkeys` | Show all keyboard shortcuts |
55
55
  | `/changelog` | Display version history |
56
- | `/quit` | Quit pi |
56
+ | `/quit` | Quit phi |
57
57
 
58
58
  ## Message Queue
59
59
 
@@ -64,20 +64,20 @@ You can submit messages while the agent is still working:
64
64
  - **Escape** aborts and restores queued messages to the editor.
65
65
  - **Alt+Up** retrieves queued messages back to the editor.
66
66
 
67
- On Windows Terminal, Alt+Enter is fullscreen by default. Remap it as described in [Terminal setup](terminal-setup.md) if you want pi to receive the shortcut.
67
+ On Windows Terminal, Alt+Enter is fullscreen by default. Remap it as described in [Terminal setup](terminal-setup.md) if you want phi to receive the shortcut.
68
68
 
69
69
  Configure delivery in [Settings](settings.md) with `steeringMode` and `followUpMode`.
70
70
 
71
71
  ## Sessions
72
72
 
73
- Sessions are saved automatically to `~/.pi/agent/sessions/`, organized by working directory.
73
+ Sessions are saved automatically to `~/.phi/agent/sessions/`, organized by working directory.
74
74
 
75
75
  ```bash
76
- pi -c # Continue most recent session
77
- pi -r # Browse and select a session
78
- pi --no-session # Ephemeral mode; do not save
79
- pi --session <path|id> # Use a specific session file or session ID
80
- pi --fork <path|id> # Fork a session into a new session file
76
+ phi -c # Continue most recent session
77
+ phi -r # Browse and select a session
78
+ phi --no-session # Ephemeral mode; do not save
79
+ phi --session <path|id> # Use a specific session file or session ID
80
+ phi --fork <path|id> # Fork a session into a new session file
81
81
  ```
82
82
 
83
83
  Useful session commands:
@@ -92,9 +92,9 @@ See [Sessions](sessions.md) and [Compaction](compaction.md) for details.
92
92
 
93
93
  ## Context Files
94
94
 
95
- Pi loads `AGENTS.md` or `CLAUDE.md` at startup from:
95
+ phi loads `AGENTS.md` or `CLAUDE.md` at startup from:
96
96
 
97
- - `~/.pi/agent/AGENTS.md` for global instructions
97
+ - `~/.phi/agent/AGENTS.md` for global instructions
98
98
  - parent directories, walking up from the current working directory
99
99
  - the current directory
100
100
 
@@ -104,8 +104,8 @@ Use context files for project conventions, commands, safety rules, and preferenc
104
104
 
105
105
  Replace the default system prompt with:
106
106
 
107
- - `.pi/SYSTEM.md` for a project
108
- - `~/.pi/agent/SYSTEM.md` globally
107
+ - `.phi/SYSTEM.md` for a project
108
+ - `~/.phi/agent/SYSTEM.md` globally
109
109
 
110
110
  Append to the default prompt without replacing it with `APPEND_SYSTEM.md` in either location.
111
111
 
@@ -120,24 +120,24 @@ If you use pi for open source work and want to publish sessions for model, promp
120
120
  ## CLI Reference
121
121
 
122
122
  ```bash
123
- pi [options] [@files...] [messages...]
123
+ phi [options] [@files...] [messages...]
124
124
  ```
125
125
 
126
126
  ### Package Commands
127
127
 
128
128
  ```bash
129
- pi install <source> [-l] # Install package, -l for project-local
130
- pi remove <source> [-l] # Remove package
131
- pi uninstall <source> [-l] # Alias for remove
132
- pi update [source|self|pi] # Update pi and packages; skips pinned packages
133
- pi update --extensions # Update packages only
134
- pi update --self # Update pi only
135
- pi update --extension <src> # Update one package
136
- pi list # List installed packages
137
- pi config # Enable/disable package resources
129
+ phi install <source> [-l] # Install package, -l for project-local
130
+ phi remove <source> [-l] # Remove package
131
+ phi uninstall <source> [-l] # Alias for remove
132
+ phi update [source|self] # Update phi and packages; skips pinned packages
133
+ phi update --extensions # Update packages only
134
+ phi update --self # Update phi only
135
+ phi update --extension <src> # Update one package
136
+ phi list # List installed packages
137
+ phi config # Enable/disable package resources
138
138
  ```
139
139
 
140
- See [Pi Packages](packages.md) for package sources and security notes.
140
+ See [Phi Packages](packages.md) for package sources and security notes.
141
141
 
142
142
  ### Modes
143
143
 
@@ -149,10 +149,10 @@ See [Pi Packages](packages.md) for package sources and security notes.
149
149
  | `--mode rpc` | RPC mode over stdin/stdout; see [RPC mode](rpc.md) |
150
150
  | `--export <in> [out]` | Export a session to HTML |
151
151
 
152
- In print mode, pi also reads piped stdin and merges it into the initial prompt:
152
+ In print mode, phi also reads piped stdin and merges it into the initial prompt:
153
153
 
154
154
  ```bash
155
- cat README.md | pi -p "Summarize this text"
155
+ cat README.md | phi -p "Summarize this text"
156
156
  ```
157
157
 
158
158
  ### Model Options
@@ -204,7 +204,7 @@ Built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.
204
204
  Combine `--no-*` with explicit flags to load exactly what you need, ignoring settings. Example:
205
205
 
206
206
  ```bash
207
- pi --no-extensions -e ./my-extension.ts
207
+ phi --no-extensions -e ./my-extension.ts
208
208
  ```
209
209
 
210
210
  ### Other Options
@@ -223,7 +223,7 @@ Prefix files with `@` to include them in the message:
223
223
 
224
224
  ```bash
225
225
  pi @prompt.md "Answer this"
226
- pi -p @screenshot.png "What's in this image?"
226
+ phi -p @screenshot.png "What's in this image?"
227
227
  pi @code.ts @test.ts "Review these files"
228
228
  ```
229
229
 
@@ -231,36 +231,36 @@ pi @code.ts @test.ts "Review these files"
231
231
 
232
232
  ```bash
233
233
  # Interactive with initial prompt
234
- pi "List all .ts files in src/"
234
+ phi "List all .ts files in src/"
235
235
 
236
236
  # Non-interactive
237
- pi -p "Summarize this codebase"
237
+ phi -p "Summarize this codebase"
238
238
 
239
239
  # Non-interactive with piped stdin
240
- cat README.md | pi -p "Summarize this text"
240
+ cat README.md | phi -p "Summarize this text"
241
241
 
242
242
  # Different model
243
- pi --provider openai --model gpt-4o "Help me refactor"
243
+ phi --provider openai --model gpt-4o "Help me refactor"
244
244
 
245
245
  # Model with provider prefix
246
- pi --model openai/gpt-4o "Help me refactor"
246
+ phi --model openai/gpt-4o "Help me refactor"
247
247
 
248
248
  # Model with thinking level shorthand
249
- pi --model sonnet:high "Solve this complex problem"
249
+ phi --model sonnet:high "Solve this complex problem"
250
250
 
251
251
  # Limit model cycling
252
- pi --models "claude-*,gpt-4o"
252
+ phi --models "claude-*,gpt-4o"
253
253
 
254
254
  # Read-only mode
255
- pi --tools read,grep,find,ls -p "Review the code"
255
+ phi --tools read,grep,find,ls -p "Review the code"
256
256
  ```
257
257
 
258
258
  ### Environment Variables
259
259
 
260
260
  | Variable | Description |
261
261
  |----------|-------------|
262
- | `PI_CODING_AGENT_DIR` | Override config directory; default is `~/.pi/agent` |
263
- | `PI_CODING_AGENT_SESSION_DIR` | Override session storage directory; overridden by `--session-dir` |
262
+ | `PHI_CODING_AGENT_DIR` | Override config directory; default is `~/.phi/agent` |
263
+ | `PHI_CODING_AGENT_SESSION_DIR` | Override session storage directory; overridden by `--session-dir` |
264
264
  | `PI_PACKAGE_DIR` | Override package directory, useful for Nix/Guix store paths |
265
265
  | `PI_OFFLINE` | Disable startup network operations, including update checks, package update checks, and install/update telemetry |
266
266
  | `PI_SKIP_VERSION_CHECK` | Skip the phi-code version update check at startup. This prevents the npm registry latest-version request |
@@ -270,7 +270,7 @@ pi --tools read,grep,find,ls -p "Review the code"
270
270
 
271
271
  ## Design Principles
272
272
 
273
- Pi keeps the core small and pushes workflow-specific behavior into extensions, skills, prompt templates, and packages.
273
+ phi keeps the core small and pushes workflow-specific behavior into extensions, skills, prompt templates, and packages.
274
274
 
275
275
  It bundles MCP support (see the MCP extension) and sub-agents. It does not include permission popups, to-dos, or background bash by default: you can build or install those workflows as extensions or packages, or use external tools such as containers and tmux.
276
276
 
package/docs/windows.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Windows Setup
2
2
 
3
- Pi requires a bash shell on Windows. Checked locations (in order):
3
+ phi requires a bash shell on Windows. Checked locations (in order):
4
4
 
5
- 1. Custom path from `~/.pi/agent/settings.json`
5
+ 1. Custom path from `~/.phi/agent/settings.json`
6
6
  2. Git Bash (`C:\Program Files\Git\bin\bash.exe`)
7
7
  3. `bash.exe` on PATH (Cygwin, MSYS2, WSL)
8
8
 
@@ -4,123 +4,15 @@
4
4
  * Provides:
5
5
  * - /agents command to list all configured sub-agents
6
6
  * - /agents <name> to show detailed agent info
7
- * - Agent definitions loaded from ~/.phi/agent/agents/ and .phi/agents/
8
- * - Model assignment visibility
7
+ * - Agent definitions loaded from .phi/agents/, ~/.phi/agent/agents/ and the
8
+ * bundled agents/ directory via the shared providers/agent-def module
9
+ * (same parser and search order as the /plan orchestrator).
9
10
  */
10
11
 
11
12
  import type { ExtensionAPI } from "phi-code";
12
- import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
13
- import { join, basename } from "node:path";
14
- import { homedir } from "node:os";
15
-
16
- interface AgentDefinition {
17
- name: string;
18
- description: string;
19
- tools: string[];
20
- model: string;
21
- source: string; // "global", "project", "bundled"
22
- filePath: string;
23
- systemPrompt: string;
24
- }
25
-
26
- /**
27
- * Parse YAML frontmatter from agent .md file
28
- */
29
- function parseAgentFile(filePath: string): AgentDefinition | null {
30
- try {
31
- const content = readFileSync(filePath, "utf-8");
32
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
33
-
34
- if (!fmMatch) return null;
35
-
36
- const frontmatter = fmMatch[1];
37
- const body = fmMatch[2].trim();
38
-
39
- // Simple YAML parser for our frontmatter
40
- const fields: Record<string, string> = {};
41
- for (const line of frontmatter.split("\n")) {
42
- const match = line.match(/^(\w+):\s*(.*)$/);
43
- if (match) {
44
- fields[match[1]] = match[2].trim();
45
- }
46
- }
47
-
48
- if (!fields.name) return null;
49
-
50
- return {
51
- name: fields.name,
52
- description: fields.description || "No description",
53
- tools: (fields.tools || "").split(",").map(t => t.trim()).filter(Boolean),
54
- model: fields.model || "default",
55
- source: "unknown",
56
- filePath,
57
- systemPrompt: body,
58
- };
59
- } catch {
60
- return null;
61
- }
62
- }
63
-
64
- /**
65
- * Scan a directory for agent .md files
66
- */
67
- function scanAgentDir(dir: string, source: string): AgentDefinition[] {
68
- const agents: AgentDefinition[] = [];
69
-
70
- if (!existsSync(dir)) return agents;
71
-
72
- try {
73
- const entries = readdirSync(dir);
74
- for (const entry of entries) {
75
- if (!entry.endsWith(".md")) continue;
76
- const fullPath = join(dir, entry);
77
- if (!statSync(fullPath).isFile()) continue;
78
-
79
- const agent = parseAgentFile(fullPath);
80
- if (agent) {
81
- agent.source = source;
82
- agents.push(agent);
83
- }
84
- }
85
- } catch {
86
- // Directory not readable
87
- }
88
-
89
- return agents;
90
- }
13
+ import { type AgentDef, discoverAgents } from "./providers/agent-def.js";
91
14
 
92
15
  export default function agentsExtension(pi: ExtensionAPI) {
93
- /**
94
- * Discover all agent definitions from all sources
95
- */
96
- function discoverAgents(): AgentDefinition[] {
97
- const seen = new Set<string>();
98
- const allAgents: AgentDefinition[] = [];
99
-
100
- const addAgents = (agents: AgentDefinition[]) => {
101
- for (const agent of agents) {
102
- if (!seen.has(agent.name)) {
103
- seen.add(agent.name);
104
- allAgents.push(agent);
105
- }
106
- }
107
- };
108
-
109
- // 1. Project-local agents
110
- addAgents(scanAgentDir(join(process.cwd(), ".phi", "agents"), "project"));
111
-
112
- // 2. Global agents
113
- addAgents(scanAgentDir(join(homedir(), ".phi", "agent", "agents"), "global"));
114
-
115
- // 3. Bundled agents (shipped with package)
116
- const bundledDir = join(__dirname, "..", "..", "..", "agents");
117
- if (existsSync(bundledDir)) {
118
- addAgents(scanAgentDir(bundledDir, "bundled"));
119
- }
120
-
121
- return allAgents;
122
- }
123
-
124
16
  /**
125
17
  * /agents command
126
18
  */
@@ -163,7 +55,7 @@ ${agent.systemPrompt.substring(0, 800)}${agent.systemPrompt.length > 800 ? "\n..
163
55
  let output = `**🤖 Sub-Agents (${agents.length})**\n\n`;
164
56
 
165
57
  // Group by source
166
- const bySource: Record<string, AgentDefinition[]> = {};
58
+ const bySource: Record<string, AgentDef[]> = {};
167
59
  for (const agent of agents) {
168
60
  const key = agent.source;
169
61
  if (!bySource[key]) bySource[key] = [];
@@ -29,6 +29,7 @@ import {
29
29
  parsePhaseVerdict,
30
30
  } from "./providers/orchestrator-helpers.js";
31
31
  import { defaultExplorerSpecs, READONLY_EXPLORER_TOOLS, runExploreFanout } from "./providers/explore-fanout.js";
32
+ import { type AgentDef, loadAgentDef } from "./providers/agent-def.js";
32
33
 
33
34
  // ─── Types ───────────────────────────────────────────────────────────────
34
35
 
@@ -212,12 +213,6 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
212
213
 
213
214
  // ─── Orchestration State ─────────────────────────────────────────
214
215
 
215
- interface AgentDef {
216
- name: string;
217
- tools: string[];
218
- systemPrompt: string;
219
- }
220
-
221
216
  interface OrchestratorPhase {
222
217
  key: string;
223
218
  label: string;
@@ -330,45 +325,18 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
330
325
  \`\`\`
331
326
  - **Internal orchestration data:** notes injected as "Previous phase summary" or budget/handoff reminders are for YOUR use only. Do not repeat them back to the user.`;
332
327
 
333
- /**
334
- * Parse agent .md file with YAML frontmatter
335
- */
336
- function loadAgentDef(name: string): AgentDef | null {
337
- const dirs = [
338
- join(process.cwd(), ".phi", "agents"),
339
- join(homedir(), ".phi", "agent", "agents"),
340
- ];
341
- for (const dir of dirs) {
342
- const filePath = join(dir, `${name}.md`);
343
- try {
344
- const content = readFileSync(filePath, "utf-8");
345
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
346
- if (!fmMatch) continue;
347
- const fields: Record<string, string> = {};
348
- for (const line of fmMatch[1].split("\n")) {
349
- const m = line.match(/^(\w+):\s*(.*)$/);
350
- if (m) fields[m[1]] = m[2].trim();
351
- }
352
- return {
353
- name: fields.name || name,
354
- tools: (fields.tools || "").split(",").map(t => t.trim()).filter(Boolean),
355
- systemPrompt: fmMatch[2].trim(),
356
- };
357
- } catch { continue; }
358
- }
359
- return null;
360
- }
361
-
362
328
  /**
363
329
  * Load a skill body (SKILL.md) so a phase instruction can embed it verbatim.
364
- * Search order mirrors loadAgentDef: project .phi/skills first, then the
365
- * global ~/.phi/agent/skills (where postinstall copies the bundled skills).
366
- * YAML frontmatter is stripped. Returns null when the skill is not installed.
330
+ * Search order mirrors agent-def.ts: project .phi/skills, then the global
331
+ * ~/.phi/agent/skills (postinstall copies bundled skills there), then the
332
+ * bundled <package>/skills for the repo layout. YAML frontmatter is
333
+ * stripped. Returns null when the skill is not installed.
367
334
  */
368
335
  function loadSkillContent(name: string): string | null {
369
336
  const dirs = [
370
337
  join(process.cwd(), ".phi", "skills"),
371
338
  join(homedir(), ".phi", "agent", "skills"),
339
+ join(__dirname, "..", "..", "skills"),
372
340
  ];
373
341
  for (const dir of dirs) {
374
342
  try {
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Agent definitions (.md with YAML frontmatter) — single source of truth for
3
+ * parsing and discovery, shared by the /plan orchestrator (loadAgentDef) and
4
+ * the /agents command (discoverAgents). Before this module existed the two
5
+ * call sites each had their own parser and they had already drifted.
6
+ *
7
+ * Search order (first match wins):
8
+ * 1. <cwd>/.phi/agents/ (project)
9
+ * 2. ~/.phi/agent/agents/ (global — postinstall copies bundled here)
10
+ * 3. <package>/agents/ (bundled, repo layout)
11
+ *
12
+ * Layout note: this file lives in extensions/phi/providers/. Three hops up is
13
+ * the package root in the repo layout (packages/coding-agent) AND the agent
14
+ * dir in the installed layout (~/.phi/agent), so the "bundled" candidate
15
+ * resolves to a real agents/ directory in both.
16
+ */
17
+
18
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { basename, join } from "node:path";
21
+
22
+ export type AgentSource = "project" | "global" | "bundled";
23
+
24
+ export interface AgentDef {
25
+ name: string;
26
+ description: string;
27
+ tools: string[];
28
+ model: string;
29
+ systemPrompt: string;
30
+ filePath: string;
31
+ source: AgentSource;
32
+ }
33
+
34
+ /**
35
+ * Parse an agent .md file: YAML frontmatter (name, description, tools, model)
36
+ * followed by the system prompt body. `tools` is a comma-separated list.
37
+ * Returns null when the file has no frontmatter block.
38
+ */
39
+ export function parseAgentMarkdown(content: string, filePath: string, source: AgentSource): AgentDef | null {
40
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
41
+ if (!fmMatch) return null;
42
+
43
+ const fields: Record<string, string> = {};
44
+ for (const line of fmMatch[1].split("\n")) {
45
+ const m = line.match(/^(\w+):\s*(.*)$/);
46
+ if (m) fields[m[1]] = m[2].trim();
47
+ }
48
+
49
+ const name = fields.name || basename(filePath).replace(/\.md$/, "");
50
+ if (!name) return null;
51
+
52
+ return {
53
+ name,
54
+ description: fields.description || "No description",
55
+ tools: (fields.tools || "")
56
+ .split(",")
57
+ .map((t) => t.trim())
58
+ .filter(Boolean),
59
+ model: fields.model || "default",
60
+ systemPrompt: fmMatch[2].trim(),
61
+ filePath,
62
+ source,
63
+ };
64
+ }
65
+
66
+ function readAgentFile(filePath: string, source: AgentSource): AgentDef | null {
67
+ try {
68
+ return parseAgentMarkdown(readFileSync(filePath, "utf-8"), filePath, source);
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /** Agent directories in precedence order (project > global > bundled). */
75
+ export function agentSearchDirs(cwd: string = process.cwd()): Array<{ dir: string; source: AgentSource }> {
76
+ return [
77
+ { dir: join(cwd, ".phi", "agents"), source: "project" },
78
+ { dir: join(homedir(), ".phi", "agent", "agents"), source: "global" },
79
+ { dir: join(__dirname, "..", "..", "..", "agents"), source: "bundled" },
80
+ ];
81
+ }
82
+
83
+ /**
84
+ * Load a single agent definition by name. Used by the /plan orchestrator to
85
+ * activate a phase persona.
86
+ */
87
+ export function loadAgentDef(name: string, cwd: string = process.cwd()): AgentDef | null {
88
+ for (const { dir, source } of agentSearchDirs(cwd)) {
89
+ const def = readAgentFile(join(dir, `${name}.md`), source);
90
+ if (def) return def;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Discover every agent definition across all sources, first match per name
97
+ * wins. Used by the /agents command.
98
+ */
99
+ export function discoverAgents(cwd: string = process.cwd()): AgentDef[] {
100
+ const seen = new Set<string>();
101
+ const agents: AgentDef[] = [];
102
+
103
+ for (const { dir, source } of agentSearchDirs(cwd)) {
104
+ if (!existsSync(dir)) continue;
105
+ let entries: string[];
106
+ try {
107
+ entries = readdirSync(dir);
108
+ } catch {
109
+ continue;
110
+ }
111
+ for (const entry of entries) {
112
+ if (!entry.endsWith(".md")) continue;
113
+ const filePath = join(dir, entry);
114
+ try {
115
+ if (!statSync(filePath).isFile()) continue;
116
+ } catch {
117
+ continue;
118
+ }
119
+ const def = readAgentFile(filePath, source);
120
+ if (def && !seen.has(def.name)) {
121
+ seen.add(def.name);
122
+ agents.push(def);
123
+ }
124
+ }
125
+ }
126
+
127
+ return agents;
128
+ }
@@ -28,12 +28,12 @@ import { join } from "node:path";
28
28
  import { homedir } from "node:os";
29
29
 
30
30
  export default function skillLoaderExtension(pi: ExtensionAPI) {
31
- // Bundled skills live inside the package in the repo layout; postinstall
32
- // copies them to ~/.phi/agent/skills (== globalDir) in the installed layout,
33
- // where the relative hop from ~/.phi/agent/extensions/ would point at the
34
- // non-existent ~/skills. Probe both (the scanner dedupes by skill name).
31
+ // Bundled skills live at <package>/skills in the repo layout (two hops up
32
+ // from extensions/phi/); postinstall copies them to ~/.phi/agent/skills
33
+ // (== globalDir) in the installed layout. Probe both (the scanner dedupes
34
+ // by skill name).
35
35
  const bundledCandidates = [
36
- join(__dirname, "..", "..", "..", "skills"),
36
+ join(__dirname, "..", "..", "skills"),
37
37
  join(homedir(), ".phi", "agent", "skills"),
38
38
  ];
39
39
  const config: SkillsConfig = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -29,6 +29,7 @@
29
29
  "skills",
30
30
  "agents",
31
31
  "extensions",
32
+ "config",
32
33
  "README.md",
33
34
  "LICENSE",
34
35
  "CHANGELOG.md",