pi-code 1.0.18 → 1.0.19
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 +1 -1
- package/extensions/skills.ts +90 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ One `pi install` and everything below loads on the next start. `pi list` shows w
|
|
|
44
44
|
| Global + project rules | `~/.claude/rules`, `.claude/rules` (nearest at or above cwd); unscoped rules inlined in full, `paths:`-scoped rules surfaced as pointers and auto-attached (the rule body is appended to a read/edit/write result when a matching file is touched, once per rule per session) | `claude-rules.ts` |
|
|
45
45
|
| Custom slash commands | `.claude/commands/**/*.md` (namespaced `/dir:name`); `$ARGUMENTS` (with the `ARGUMENTS:` append when unused), 0-based `$ARGUMENTS[N]`/`$N`, named `arguments:` frontmatter, `${CLAUDE_SESSION_ID}`/`${CLAUDE_EFFORT}`/`${CLAUDE_SKILL_DIR}`/`${CLAUDE_PROJECT_DIR}` (in bodies and `allowed-tools` rules); `` !`cmd` `` and multi-line ```` ```! ```` bash (whitespace-bounded, merged stderr, 2-minute budget, a failure aborts the invocation with the documented exit-1 carveout), `@file` inlining; `allowed-tools` with `Bash(...)` and `Read`/`Edit`/`Write` path scopes enforced at call time (gitignore anchors, Edit governs writes), `disallowed-tools`, `argument-hint`, `model` (switches the session model for the command's turn, restored after), `effort` (raises reasoning for the turn, restored after), `shell: powershell` (injected spans run through PowerShell when a `pwsh` binary is present, else `/bin/sh`); the model can also run a command itself through the `SlashCommand` tool (Claude's `SlashCommand` in `allowed-tools`), steered by `when_to_use` and opted out per file with `disable-model-invocation` (`user-invocable: false` hides a command from the menu while still exposing it to the model); `disableSkillShellExecution` (managed and user always, project when trusted) replaces every `!` span with a policy-disabled placeholder; project commands gated on approval | `commands.ts` |
|
|
46
46
|
| `/init` | generates a project context file: detects an existing `AGENTS.md`/`CLAUDE.md` (proposes improvements) or none (creates `AGENTS.md`, pi's preferred name), ingesting `.cursor/rules`, `.cursorrules`, and `.github/copilot-instructions.md` when present; drives the main agent with full tools via a prompt (not a tool-less completion) so it analyzes the codebase and writes the file itself | `init.ts` |
|
|
47
|
-
| Skills | `.claude/skills` → pi skill discovery, project skills gated on approval (pi reads `name`, `description`, `disable-model-invocation`; `allowed-tools` is inert in pi's loader) | `skills.ts` |
|
|
47
|
+
| Skills | `.claude/skills` → pi skill discovery, project skills gated on approval (pi reads `name`, `description`, `disable-model-invocation`; `allowed-tools` is inert in pi's loader); invoking `/skill:name` expands the body through the command pipeline, as Claude documents commands and skills "work the same way": `` !`cmd` `` spans, `@file` references, `$ARGUMENTS`/positional substitution and `${CLAUDE_*}` variables, with a malformed-frontmatter skill degrading to pi's plain expansion | `skills.ts` |
|
|
48
48
|
| Hooks | `.claude/settings.json` hooks: PreToolUse (blocks, rewrites input via `updatedInput`), PostToolUse (feedback and `additionalContext` land next to the tool result), PostToolUseFailure, SessionStart (context injection), UserPromptSubmit (blocks and injects context), Stop (a block continues the conversation), SubagentStart/SubagentStop, PreCompact, PostCompact, SessionEnd, Notification (idle_prompt, the type pi can source), InstructionsLoaded (observational: fires per loaded context file at session start, plus `path_glob_match` on a scoped-rule attach and `include` per resolved `@import`; deduped per session; `nested_traversal`/`compact` reasons never fire since pi does not lazily load nested CLAUDE.md or reload after compaction); `type: http` entries POST the payload (a 2xx JSON body renders the decision, everything else is non-blocking per Claude's contract), `type: prompt` evaluates in-process against the session model, `type: mcp_tool` calls a connected server's tool, and `type: agent` (experimental) spawns a read-only Read/Grep/Glob subagent that returns the JSON decision (a missing model/server/runner is non-blocking, only a PreToolUse timeout fails closed); Claude matcher semantics incl. `mcp__server__tool` names; payloads carry session_id, transcript_path, cwd, permission_mode, effort; `permissionDecision: "ask"` prompts via a confirm dialog (blocks when headless), SubagentStop/PostToolUseFailure are notify-only, and a timed-out PreToolUse/UserPromptSubmit hook fails closed at a 60s default (Claude: 600s for PreToolUse, 30s for UserPromptSubmit, both non-blocking) since pi has no permission backstop; `async`/`asyncRewake` command hooks run in the background on every event (never blocking, no decision, no timeout enforced on `async` while asyncRewake keeps its own; an asyncRewake exit 2 wakes the model with the hook's stderr as a new turn, other completions deliver `systemMessage`/`additionalContext` to the model on the next turn, and hooks still running at session end are killed); payloads use Claude's vocabulary for pi's built-ins (Bash/Edit/Write/Read/Grep/Glob names, documented input and Bash/Write response shapes, absolute `file_path`, `updatedInput` translated back); PreToolUse `additionalContext` lands beside the tool result, `updatedToolOutput`/`updatedMCPToolOutput` replace what the model sees, `permissionDecision: "defer"` blocks (pi cannot resume a deferred call), the `if` permission-rule filter runs on tool events only (a hook carrying it never runs elsewhere), Stop/UserPromptSubmit ignore stray matchers, and a Stop hook's `additionalContext` continues the conversation under the block cap; a `command` hook may use exec form (`command` as an argv array, run with no shell) and executes with `CLAUDECODE=1` and `CLAUDE_PROJECT_DIR` set; a user-typed `!`/`!!` bash line runs PreToolUse (there is no PostToolUse for it); `type: http` targets are gated by `allowedHttpHookUrls` (union of managed and settings scopes; unset allows all, `[]` blocks every http hook); a Stop hook may block at most 8 times before the turn ends (`CLAUDE_CODE_STOP_HOOK_BLOCK_CAP`); `disableAllHooks` in any scope turns the system off; `/hooks` prints the resolved configuration | `hooks.ts` |
|
|
49
49
|
| Output styles | `.claude/output-styles` + active `outputStyle`; plus styles shipped by enabled plugins (manifest `outputStyles`, default `output-styles/`, ranked below the user's and project's own); Claude replace semantics with `keep-coding-instructions`; bundled Explanatory/Learning/Proactive; `/output-style [name]` | `output-styles.ts` |
|
|
50
50
|
| CLAUDE.md `@imports` and rewriting | resolves `@path` imports pi's native loader skips (4-hop depth, budget-capped); loads the user `~/.claude/CLAUDE.md` and the project `.claude/CLAUDE.md` (approval-gated, deduped against the repo-root `CLAUDE.md`/`AGENTS.md` pi loads natively) that pi's own loader does not; loads every `CLAUDE.local.md` from the repo root down to cwd (approval-gated, root first); injects the managed `CLAUDE.md` (a per-OS file beside `managed-settings.json`) and the `claudeMd` string from `managed-settings.json` at the top of context (managed content is never excludable); honors `claudeMdExcludes` (glob/absolute-path skip list from user, approved-project, and managed settings, merged; managed content never excluded); strips block-level HTML comments from CLAUDE.md, rule, and imported bodies (fenced-code comments preserved), so a commented-out `@import` does not expand; with `CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD` set, loads `CLAUDE.md`/`.claude/CLAUDE.md`/`.claude/rules/*.md`/`CLAUDE.local.md` from each `--add-dir` directory (comma-separated for several, since pi's flag is single-value) | `context-imports.ts` |
|
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.19",
|
|
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",
|