pi-code 1.0.18 → 1.0.20
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.
- package/README.md +16 -26
- package/extensions/skills.ts +90 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,31 +39,21 @@ One `pi install` and everything below loads on the next start. `pi list` shows w
|
|
|
39
39
|
|
|
40
40
|
## What it does
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| Checkpoints / rewind | shadow-repo snapshots; restore overwrites checkpointed files, keeps files created later; 100 per session, repos pruned after 30 days | `git-checkpoint.ts` |
|
|
58
|
-
| Persistent memory | per-repo memories under `~/.pi/agent/memory` keyed on the repository root (subdirectory sessions share one store, as Claude does; pi's own store, separate from Claude's), index injected each session within Claude's 200-line/25KB bound (YAML frontmatter and block HTML comments stripped before it counts or loads); a save that would overflow it reports why; a memory written with frontmatter gets a `modified:` ISO timestamp; honors `autoMemoryEnabled` (settings) and `CLAUDE_CODE_DISABLE_AUTO_MEMORY` (env) to turn it off, and `autoMemoryDirectory` (absolute or `~/`) to relocate the store | `memory.ts` |
|
|
59
|
-
| WebSearch / WebFetch | key-free DuckDuckGo search (with `allowed_domains`/`blocked_domains`); SSRF-guarded fetch that prefers markdown via `Accept` then converts HTML, with Claude's 15-minute per-URL cache and an optional `prompt` that runs the page through the model in-process and returns the answer (falls back to markdown when headless or on error) | `web.ts` |
|
|
60
|
-
| AskUserQuestion | 1-4 questions per call (asked in sequence), each with `header` and 2-4 options, single- or `multiSelect`, plus free-text | `question.ts` |
|
|
61
|
-
| Statusline | Claude `statusLine` command contract (stdin JSON incl. `version`, `hook_event_name`, `session_name`, a `cost` block with wall and API durations and lines added/removed, a `context_window` token breakdown, `exceeds_200k_tokens`, and `rate_limits` carrying the `five_hour`/`seven_day` utilization and reset from the provider's rate-limit headers; `padding`, `refreshInterval`, 300ms debounce); built-in turn state + session cost fallback | `status-line.ts` |
|
|
62
|
-
| Notifications | terminal notification when a turn ends (OSC 777 / Kitty OSC 99 / Windows toast); honors `preferredNotifChannel` (`terminal_bell`, `notifications_disabled`, `iterm2_with_bell`, else desktop) from user settings; fires only when you "appear to be away" (approximated by turn duration, since pi exposes no terminal-focus signal) | `notify.ts` |
|
|
63
|
-
| Think keywords | raises reasoning for one turn when the prompt carries a keyword: `ultrathink` to the max, `think hard`/`think harder` to high, a bare `think` to medium; only ever raises, matched on word boundaries, and the prior level is restored once the turn settles | `thinking.ts` |
|
|
64
|
-
| Session title | names a new session from its first message with a single model call (shown in the session selector and the terminal title); never overwrites an existing name, best-effort, at most once per session | `session-title.ts` |
|
|
65
|
-
| `/context` | reports how much of the model's context window the session occupies (used, window, free, and percent), and reads pi's live usage so it reflects a compaction | `context-usage.ts` |
|
|
66
|
-
| Claude plugins | installed marketplace plugins (`~/.claude/plugins/cache`), active per `enabledPlugins` in user settings only (a checked-out repo cannot flip which code-bearing plugins run, so project settings never toggle them): commands as `/plugin:name`, agents, hooks and MCP servers (tools aliased `mcp__plugin_<plugin>_<server>__<tool>`) and output styles with `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PLUGIN_DATA}` and `${user_config.KEY}` (from `pluginConfigs[id].options` in user settings) substituted; skill dirs contribute too, though pi's loader names them without the plugin prefix | `internal/plugins.ts` |
|
|
42
|
+
Each topic links to its own doc with the full contract and any divergences from Claude Code.
|
|
43
|
+
|
|
44
|
+
- **[Hooks](docs/hooks.md)** — your `.claude/settings.json` hooks on every lifecycle event, with Claude's tool vocabulary, decision fields, and background hooks.
|
|
45
|
+
- **[MCP servers](docs/mcp.md)** — every Claude config scope, all four transports, OAuth, managed policy, timeouts, prompts, and resources.
|
|
46
|
+
- **[Custom slash commands](docs/commands.md)** — `.claude/commands` with arguments, bash spans, `@file` inlining, frontmatter, and model invocation.
|
|
47
|
+
- **[Skills](docs/skills.md)** — `.claude/skills` discovery plus the same dynamic content commands get.
|
|
48
|
+
- **[Subagents / Task](docs/subagents.md)** — built-in and custom agents, background runs, per-agent memory, worktree isolation.
|
|
49
|
+
- **[CLAUDE.md, @imports, and rules](docs/claude-md.md)** — the context files and path-scoped rules pi does not load natively.
|
|
50
|
+
- **[Settings `env`](docs/settings-env.md)** — env blocks from every settings scope, exported with Claude's precedence.
|
|
51
|
+
- **[Output styles](docs/output-styles.md)** — replace semantics, bundled built-ins, `/output-style`.
|
|
52
|
+
- **[Persistent memory](docs/memory.md)** — a per-repository store with a session-injected index.
|
|
53
|
+
- **[Statusline](docs/statusline.md)** — your Claude `statusLine` command with the documented stdin JSON.
|
|
54
|
+
- **[WebSearch / WebFetch](docs/web.md)** — key-free search and SSRF-guarded fetch.
|
|
55
|
+
- **[Claude plugins](docs/plugins.md)** — installed marketplace plugins: commands, agents, hooks, MCP servers, styles, skills.
|
|
56
|
+
- **[Session extras](docs/session-extras.md)** — project trust, plan mode, todos, checkpoints/rewind, AskUserQuestion, notifications, think keywords, session titles, `/context`, `/init`.
|
|
67
57
|
|
|
68
58
|
Slash commands: `/init`, `/context`, `/memory`, `/todos`, `/rewind`, `/tasks`, `/agents`, `/plan`, `/mcp`, `/hooks`, and `/output-style`, alongside your own `/dir:name` commands, `/skill:name` skills, `/plugin:name` plugin commands, and each connected server's `/mcp__server__prompt` prompts.
|
|
69
59
|
|
|
@@ -71,7 +61,7 @@ pi has no general permission system, so most of what Claude routes through a per
|
|
|
71
61
|
|
|
72
62
|
`CLAUDE.md` itself needs no extension: pi loads `CLAUDE.md` / `AGENTS.md` context files natively (global + walking cwd to root). `context-imports.ts` only adds the `@import` resolution pi's loader lacks, appending the imported files without re-injecting the base. Setting `CLAUDE_CONFIG_DIR` relocates the entire home config scope (settings, commands, agents, skills, plugins, output styles, memory, and the user `CLAUDE.md`); a project's own `.claude/` is a separate scope and is unaffected.
|
|
73
63
|
|
|
74
|
-
`extensions/internal/` holds shared modules pi's loader must not treat as extensions
|
|
64
|
+
[`extensions/internal/`](extensions/internal) holds the shared modules pi's loader must not treat as extensions (each file's header says what it owns); only `internal/` keeps them out of pi's extension scan.
|
|
75
65
|
|
|
76
66
|
Vendored bases (`question`, `notify`, `status-line`) come from pi's MIT example extensions (see [LICENSE](LICENSE)).
|
|
77
67
|
|
package/extensions/skills.ts
CHANGED
|
@@ -7,14 +7,25 @@
|
|
|
7
7
|
* pi implements the Agent Skills standard, so `SKILL.md` directories work
|
|
8
8
|
* unchanged and register as `/skill:name`.
|
|
9
9
|
*
|
|
10
|
+
* Claude documents that a command file and a skill "work the same way", so a
|
|
11
|
+
* SKILL.md body carries the dynamic features command bodies do: `` !`cmd` ``
|
|
12
|
+
* spans, `@file` references, `$ARGUMENTS`/positional substitution and
|
|
13
|
+
* `${CLAUDE_*}` variables. pi's loader delivers the raw text, so this extension
|
|
14
|
+
* intercepts `/skill:name` input for the skills it contributed, expands the body
|
|
15
|
+
* through the shared command pipeline, and hands pi the already-expanded content
|
|
16
|
+
* in pi's own skill-block format (pi emits the input event before its own
|
|
17
|
+
* expansion, and skips text that no longer starts with `/`).
|
|
18
|
+
*
|
|
10
19
|
* Docs: https://code.claude.com/docs/en/skills.md, https://agentskills.io
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
22
|
import * as fs from 'node:fs'
|
|
14
23
|
import * as os from 'node:os'
|
|
15
24
|
import * as path from 'node:path'
|
|
16
|
-
import type
|
|
25
|
+
import { type ExtensionAPI, type ExtensionContext, parseFrontmatter } from '@earendil-works/pi-coding-agent'
|
|
17
26
|
|
|
27
|
+
import { expandCommand, shellExecutionDisabled } from './commands.js'
|
|
28
|
+
import { parseCommandFile } from './internal/command-file.js'
|
|
18
29
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
19
30
|
import { installedPlugins } from './internal/plugins.js'
|
|
20
31
|
import { isProjectApprovedSilently } from './internal/project-approval.js'
|
|
@@ -28,7 +39,6 @@ function isDirectory(target: string): boolean {
|
|
|
28
39
|
}
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
/** Existing `.claude/skills` directories, user first then project. */
|
|
32
42
|
/** Existing `.claude/skills` directories, user first then project. The project
|
|
33
43
|
* directory is included only for approved projects: pi's loader surfaces every skill's
|
|
34
44
|
* name and description to the model, so an untrusted repository would otherwise get
|
|
@@ -51,6 +61,50 @@ export function skillDirs(cwd: string, home: string, trusted: boolean): string[]
|
|
|
51
61
|
return dirs
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
interface FoundSkill {
|
|
65
|
+
filePath: string
|
|
66
|
+
baseDir: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The skill a directory entry holds, named as pi's loader names it (frontmatter
|
|
70
|
+
* `name`, else the directory name); undefined without a readable SKILL.md. */
|
|
71
|
+
function skillAt(root: string, dirName: string): { name: string; filePath: string } | undefined {
|
|
72
|
+
const filePath = path.join(root, dirName, 'SKILL.md')
|
|
73
|
+
let content: string
|
|
74
|
+
try {
|
|
75
|
+
content = fs.readFileSync(filePath, 'utf-8')
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
let name = dirName
|
|
80
|
+
try {
|
|
81
|
+
const declared = parseFrontmatter<Record<string, unknown>>(content).frontmatter.name
|
|
82
|
+
if (typeof declared === 'string' && declared.trim()) name = declared.trim()
|
|
83
|
+
} catch {
|
|
84
|
+
// Malformed frontmatter: pi's loader falls back to the directory name too.
|
|
85
|
+
}
|
|
86
|
+
return { name, filePath }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A Claude-contributed skill by the name pi's loader gives it. One directory
|
|
90
|
+
* level, the standard layout. */
|
|
91
|
+
export function findClaudeSkill(name: string, roots: string[]): FoundSkill | undefined {
|
|
92
|
+
for (const root of roots) {
|
|
93
|
+
let entries: fs.Dirent[]
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
96
|
+
} catch {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.isDirectory()) continue
|
|
101
|
+
const skill = skillAt(root, entry.name)
|
|
102
|
+
if (skill?.name === name) return { filePath: skill.filePath, baseDir: path.dirname(skill.filePath) }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
export default function skillsExtension(pi: ExtensionAPI) {
|
|
55
109
|
pi.on('resources_discover', async (_event, ctx) => {
|
|
56
110
|
// resources_discover fires after session_start, so the approval is already
|
|
@@ -58,4 +112,38 @@ export default function skillsExtension(pi: ExtensionAPI) {
|
|
|
58
112
|
const skillPaths = skillDirs(ctx.cwd, os.homedir(), isProjectApprovedSilently(ctx))
|
|
59
113
|
return skillPaths.length > 0 ? { skillPaths } : undefined
|
|
60
114
|
})
|
|
115
|
+
|
|
116
|
+
// The dynamic-content shim: only for skills this extension contributed; pi's own
|
|
117
|
+
// `.pi/skills` (or an unknown name) pass through to pi's plain expansion.
|
|
118
|
+
pi.on('input', async (event, ctx) => {
|
|
119
|
+
if (event.source === 'extension') return
|
|
120
|
+
return expandSkillInvocation(pi, event.text, ctx)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A `/skill:name args` invocation into its expanded skill block, or undefined to
|
|
125
|
+
* pass the input through to pi untouched. The expanded body is wrapped in pi's
|
|
126
|
+
* skill-block format so downstream behavior (the baseDir note for relative
|
|
127
|
+
* references) matches an untouched invocation. */
|
|
128
|
+
async function expandSkillInvocation(pi: ExtensionAPI, rawText: string, ctx: ExtensionContext): Promise<{ action: 'transform'; text: string } | undefined> {
|
|
129
|
+
const text = rawText.trimStart()
|
|
130
|
+
if (!text.startsWith('/skill:')) return
|
|
131
|
+
const space = text.indexOf(' ')
|
|
132
|
+
const name = (space === -1 ? text.slice(7) : text.slice(7, space)).trim()
|
|
133
|
+
const args = space === -1 ? '' : text.slice(space + 1).trim()
|
|
134
|
+
if (!name) return
|
|
135
|
+
const trusted = isProjectApprovedSilently(ctx)
|
|
136
|
+
const found = findClaudeSkill(name, skillDirs(ctx.cwd, os.homedir(), trusted))
|
|
137
|
+
if (!found) return
|
|
138
|
+
let parsed: ReturnType<typeof parseCommandFile>
|
|
139
|
+
try {
|
|
140
|
+
parsed = parseCommandFile(fs.readFileSync(found.filePath, 'utf-8'))
|
|
141
|
+
} catch {
|
|
142
|
+
// Unreadable, or malformed frontmatter: pass through to pi's plain expansion
|
|
143
|
+
// (the loader registered the skill and delivers the raw body), rather than
|
|
144
|
+
// failing the invocation over the dynamic features it cannot have.
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
const expanded = await expandCommand(pi, parsed, args, { cwd: ctx.cwd }, found.filePath, undefined, { allowShell: !shellExecutionDisabled(ctx.cwd, os.homedir(), trusted) })
|
|
148
|
+
return { action: 'transform', text: `<skill name="${name}" location="${found.filePath}">\nReferences are relative to ${found.baseDir}.\n\n${expanded}\n</skill>` }
|
|
61
149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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",
|