pi-code 1.0.3 → 1.0.5
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 +25 -13
- package/extensions/claude-rules.ts +158 -54
- package/extensions/commands.ts +185 -27
- package/extensions/context-imports.ts +358 -41
- package/extensions/git-checkpoint.ts +22 -1
- package/extensions/hooks.ts +397 -79
- package/extensions/init.ts +81 -0
- package/extensions/internal/agent-run.ts +42 -0
- package/extensions/internal/bash-rules.ts +27 -0
- package/extensions/internal/command-file.ts +377 -53
- package/extensions/internal/html-markdown.ts +61 -0
- package/extensions/internal/instruction-events.ts +70 -0
- package/extensions/internal/managed-settings.ts +38 -0
- package/extensions/internal/mcp-call.ts +28 -0
- package/extensions/internal/mcp-oauth.ts +171 -0
- package/extensions/internal/model-complete.ts +68 -0
- package/extensions/internal/path-rules.ts +80 -0
- package/extensions/internal/plugins.ts +125 -0
- package/extensions/internal/project-approval.ts +2 -3
- package/extensions/internal/project-root.ts +78 -0
- package/extensions/internal/shell-split.ts +65 -0
- package/extensions/internal/strip-comments.ts +77 -0
- package/extensions/internal/web-transport.ts +3 -1
- package/extensions/mcp.ts +290 -31
- package/extensions/memory.ts +168 -23
- package/extensions/notify.ts +78 -5
- package/extensions/output-styles.ts +34 -6
- package/extensions/plan-mode/index.ts +55 -9
- package/extensions/plan-mode/utils.ts +3 -57
- package/extensions/question.ts +2 -2
- package/extensions/skills.ts +11 -1
- package/extensions/status-line.ts +97 -4
- package/extensions/subagent/agents.ts +72 -61
- package/extensions/subagent/background.ts +114 -25
- package/extensions/subagent/index.ts +227 -44
- package/extensions/web.ts +87 -16
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Extension
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's /init: analyze the codebase and produce a project context file
|
|
5
|
+
* with build commands, test instructions, and the conventions it discovers; when
|
|
6
|
+
* a context file already exists, suggest improvements instead of overwriting it.
|
|
7
|
+
* The handler does no analysis itself. Claude drives /init through the main agent
|
|
8
|
+
* with tools, and pi's equivalent is pi.sendUserMessage: the handler detects what
|
|
9
|
+
* exists on disk (existing context file, cursor rules, copilot instructions),
|
|
10
|
+
* builds one curated prompt, and sends it; the agent then explores the codebase
|
|
11
|
+
* and writes the file with full tool access. A fresh file is AGENTS.md, the name
|
|
12
|
+
* pi prefers over CLAUDE.md.
|
|
13
|
+
*
|
|
14
|
+
* Docs: https://code.claude.com/docs/en/memory.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs'
|
|
18
|
+
import * as path from 'node:path'
|
|
19
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
20
|
+
|
|
21
|
+
import { repoRoot } from './internal/project-root.js'
|
|
22
|
+
|
|
23
|
+
/** Context files pi recognizes, in its lookup order; the first hit wins. */
|
|
24
|
+
export const CONTEXT_FILE_CANDIDATES = ['AGENTS.override.md', 'AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE.MD']
|
|
25
|
+
|
|
26
|
+
function statOf(target: string): fs.Stats | undefined {
|
|
27
|
+
try {
|
|
28
|
+
return fs.statSync(target)
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The name of the first context-file candidate present at the project root. */
|
|
35
|
+
export function findExistingContextFile(root: string): string | undefined {
|
|
36
|
+
return CONTEXT_FILE_CANDIDATES.find((name) => statOf(path.join(root, name))?.isFile())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface InitPromptOptions {
|
|
40
|
+
/** Name of the context file already at the project root, when one exists. */
|
|
41
|
+
existingContextFile?: string
|
|
42
|
+
/** Whether .cursor/rules/ or .cursorrules exists at the project root. */
|
|
43
|
+
cursorRules?: boolean
|
|
44
|
+
/** Whether .github/copilot-instructions.md exists at the project root. */
|
|
45
|
+
copilotRules?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The prompt /init sends to the main agent. Pure so tests can pin its contract. */
|
|
49
|
+
export function buildInitPrompt(opts: InitPromptOptions = {}): string {
|
|
50
|
+
const parts: string[] = [
|
|
51
|
+
[
|
|
52
|
+
'Please analyze this codebase and distill what a coding agent needs to work in it effectively:',
|
|
53
|
+
'- Build, lint, and test commands, including how to run a single test.',
|
|
54
|
+
'- Code conventions: formatting, imports, naming, types, error handling.',
|
|
55
|
+
'- Architecture notes: the big-picture structure that takes reading several files to see.',
|
|
56
|
+
'- Project layout: the important directories and entry points.',
|
|
57
|
+
].join('\n'),
|
|
58
|
+
]
|
|
59
|
+
if (opts.cursorRules) parts.push('Cursor rules exist in this repository (.cursor/rules/ or .cursorrules). Read them and fold the parts that still apply into the context file.')
|
|
60
|
+
if (opts.copilotRules) parts.push('Copilot instructions exist at .github/copilot-instructions.md. Read them and fold the parts that still apply into the context file.')
|
|
61
|
+
if (opts.existingContextFile) {
|
|
62
|
+
parts.push(`A context file already exists at the project root: ${opts.existingContextFile}. Read it, then propose improvements based on what you found and apply the ones that clearly help. Do not overwrite the file wholesale; keep its structure and voice and make targeted edits.`)
|
|
63
|
+
} else {
|
|
64
|
+
parts.push('Write the result to AGENTS.md at the project root. Create the file yourself. pi prefers AGENTS.md over CLAUDE.md as its context file, and Claude Code can read it too via an @AGENTS.md import from CLAUDE.md or a symlink.')
|
|
65
|
+
}
|
|
66
|
+
parts.push('Keep the file concise and high-signal, under roughly 200 lines. Skip generic advice and anything obvious from a glance at the code.')
|
|
67
|
+
return parts.join('\n\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default function initExtension(pi: ExtensionAPI) {
|
|
71
|
+
pi.registerCommand('init', {
|
|
72
|
+
description: 'Analyze the codebase and create or improve the project context file (AGENTS.md)',
|
|
73
|
+
handler: async (_args, ctx) => {
|
|
74
|
+
const root = repoRoot(ctx.cwd) ?? ctx.cwd
|
|
75
|
+
const existing = findExistingContextFile(root)
|
|
76
|
+
const cursorRules = statOf(path.join(root, '.cursor', 'rules'))?.isDirectory() === true || statOf(path.join(root, '.cursorrules'))?.isFile() === true
|
|
77
|
+
const copilotRules = statOf(path.join(root, '.github', 'copilot-instructions.md'))?.isFile() === true
|
|
78
|
+
pi.sendUserMessage(buildInitPrompt({ ...(existing !== undefined ? { existingContextFile: existing } : {}), cursorRules, copilotRules }))
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A one-function seam letting the hooks extension spawn a subagent for Claude's
|
|
3
|
+
* experimental `type: "agent"` hooks, without importing the subagent extension.
|
|
4
|
+
*
|
|
5
|
+
* The subagent extension registers a runner once it is loaded; a hook that needs
|
|
6
|
+
* to verify a condition with a tool-using agent (Read/Grep/Glob) goes through
|
|
7
|
+
* runAgent. It is the agentic analogue of the mcp-call seam: if nothing registered
|
|
8
|
+
* a runner (a stripped-down deployment without the subagent extension), agent hooks
|
|
9
|
+
* are skipped rather than failing the event.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface AgentRunRequest {
|
|
13
|
+
/** The agent's task prompt, with `$ARGUMENTS` already substituted. */
|
|
14
|
+
prompt: string
|
|
15
|
+
/** Optional model id override; the runner falls back to a fast default. */
|
|
16
|
+
model?: string
|
|
17
|
+
/** Optional extra system prompt (Claude's experimental `systemPrompt` field). */
|
|
18
|
+
systemPrompt?: string
|
|
19
|
+
/** Aborts the run at the hook's deadline. */
|
|
20
|
+
signal?: AbortSignal
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Run a subagent to completion and return its final assistant text. */
|
|
24
|
+
export type AgentRunner = (request: AgentRunRequest) => Promise<string>
|
|
25
|
+
|
|
26
|
+
let runner: AgentRunner | undefined
|
|
27
|
+
|
|
28
|
+
/** The subagent extension registers its runner here; pass undefined to clear it. */
|
|
29
|
+
export function setAgentRunner(fn: AgentRunner | undefined): void {
|
|
30
|
+
runner = fn
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether a runner is registered, so agent hooks can be reported as runnable. */
|
|
34
|
+
export function hasAgentRunner(): boolean {
|
|
35
|
+
return runner !== undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Run a subagent for an agent hook. Rejects when no runner is registered. */
|
|
39
|
+
export function runAgent(request: AgentRunRequest): Promise<string> {
|
|
40
|
+
if (!runner) return Promise.reject(new Error('no subagent runner registered for agent hooks'))
|
|
41
|
+
return runner(request)
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude's `Bash(...)` argument scopes, enforced at tool_call time.
|
|
3
|
+
*
|
|
4
|
+
* A scope like `git add:*` narrows a bash grant to matching commands. pi's
|
|
5
|
+
* active-tool set has no argument dimension, so commands.ts grants `bash` and
|
|
6
|
+
* checks each call here instead. Matching follows Claude's documented forms:
|
|
7
|
+
* a bare specifier is an exact match, `prefix:*` matches any command starting
|
|
8
|
+
* with the prefix, and `*` elsewhere is a wildcard. Every segment of a compound
|
|
9
|
+
* command must match some rule, and substitution or an unbalanced quote fails
|
|
10
|
+
* closed, as plan mode's guard does.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hasSubstitution, splitSegments } from './shell-split.js'
|
|
14
|
+
|
|
15
|
+
const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
|
|
16
|
+
|
|
17
|
+
function matchesRule(segment: string, rule: string): boolean {
|
|
18
|
+
if (rule.endsWith(':*')) return segment.startsWith(rule.slice(0, -2))
|
|
19
|
+
if (rule.includes('*')) return new RegExp(`^${rule.split('*').map(escapeRegExp).join('[^]*')}$`).test(segment)
|
|
20
|
+
return segment === rule
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function matchesBashRules(command: string, rules: string[]): boolean {
|
|
24
|
+
if (hasSubstitution(command)) return false
|
|
25
|
+
const segments = splitSegments(command)
|
|
26
|
+
return segments.length > 0 && segments.every((segment) => rules.some((rule) => matchesRule(segment, rule.trim())))
|
|
27
|
+
}
|