@fradser/pi-context 0.5.1 → 0.6.1
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 +22 -62
- package/extensions/context-command.ts +4 -110
- package/extensions/context-prompt.ts +75 -0
- package/extensions/context-tools.ts +201 -247
- package/index.ts +2 -2
- package/package.json +10 -7
- package/prompts/context-research.md +10 -0
- package/references/workflow.md +4 -105
- package/agents/context-researcher.md +0 -36
package/README.md
CHANGED
|
@@ -1,91 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fradser/pi-context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@fradser/pi-context` exposes one tool: `context_get`.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
The package does **not** ship a skill. Native tool guidance is injected into the system prompt, and the `/context` command loads the full workflow into the current turn.
|
|
5
|
+
It starts an independent Pi child process for repository, library, codebase, and technical-topic research. This isolates exploratory tool calls from the main session while returning a concise, evidence-based result.
|
|
8
6
|
|
|
9
7
|
## Installation
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
10
|
pi install npm:@fradser/pi-context
|
|
13
|
-
# or from this repository:
|
|
14
|
-
pi install /path/to/pi-packages/packages/context
|
|
15
11
|
```
|
|
16
12
|
|
|
17
13
|
## Usage
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
Use natural language to ask Pi for research or external context:
|
|
20
16
|
|
|
21
17
|
```text
|
|
22
|
-
/
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
Research how facebook/react implements server actions.
|
|
19
|
+
Compare the current React Router and Next.js data-loading guidance.
|
|
20
|
+
Investigate https://github.com/owner/repo and explain its architecture.
|
|
25
21
|
```
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
The full workflow (target classification, per-method process, fallbacks, selection guide) lives in `references/workflow.md` and is injected into the turn when `/context` runs. Lightweight tool-selection guidance is always present in the system prompt.
|
|
30
|
-
|
|
31
|
-
## Runtime requirements
|
|
32
|
-
|
|
33
|
-
| Method | Availability | Notes |
|
|
34
|
-
|--------|--------------|-------|
|
|
35
|
-
| DeepWiki | Always (native tool `context_deepwiki`) | No API key required |
|
|
36
|
-
| Context7 | Always (native tool `context_context7`) | Optional `CONTEXT7_API_KEY` (Bearer) for per-key quota |
|
|
37
|
-
| Exa | Native tool `context_exa` | Requires `EXA_API_KEY` env var |
|
|
38
|
-
| Git clone | Always | Pi `bash` + `read` |
|
|
39
|
-
| Web fetch | Always | `curl`/HTTP via `bash` |
|
|
23
|
+
Pi recognizes these requests and invokes `context_get` automatically. Users do not need to type `context_get` or use a slash command directly; it is an internal native tool with one `query` parameter containing the context request.
|
|
40
24
|
|
|
41
|
-
|
|
25
|
+
## Child-process constraints
|
|
42
26
|
|
|
43
|
-
|
|
27
|
+
The child runs as:
|
|
44
28
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
4. **Git clone** — `git clone --depth=1` to `/tmp`, `read` key files, clean up.
|
|
49
|
-
5. **Web search+fetch** — `bash` + `curl` against official docs, GitHub issues, changelogs.
|
|
50
|
-
|
|
51
|
-
If a native tool is unavailable or errors, fall back to clone/web. The `/context` workflow reference contains the full selection matrix and per-method process.
|
|
29
|
+
```text
|
|
30
|
+
pi --print --mode json --no-session -ne -ns -np -nc --no-themes --tools read,bash
|
|
31
|
+
```
|
|
52
32
|
|
|
53
|
-
|
|
33
|
+
The child runs in the caller's working directory through pi-kit's minimal `runPiWorker` mode: `-ne -ns -np -nc --no-themes` disables extension, skill, prompt-template, context-file, and theme discovery, while `--tools read,bash` exposes only the two required tools. It has no sandbox, timeout, or display truncation; `bash` remains technically able to write because there is no OS sandbox, so the bundled worker prompt requires no modifications. If line-level evidence from a public repository is necessary, it may use `git clone --depth=1` under `/tmp` with removal after inspection. It must not run package-management, deployment, or interactive commands. The shared worker fails closed if its bounded stdout, stderr, or JSONL line limits are exceeded.
|
|
54
34
|
|
|
55
|
-
|
|
35
|
+
A package-local typed builder reads the bundled Markdown as a reference protocol and builds a task-specific context-review prompt containing the caller working directory, complete research request, reference protocol, and completion contract. The Markdown file itself is never used as an execution identity. Startup renders `[context] research started · <research query>`, using the normalized concrete query that produced the prompt; the row remains width-bounded and does not expose the resource path. While the async child runs, the transcript keeps only that started row with no duplicate progress row; live progress shows in the researcher widget above the editor. The widget shows neutral live research activity. The child always runs with `--no-session`, so separate invocations retain no memory or agent-teams state. A successful empty answer is retried exactly once with a stronger completion requirement that does not rely on hidden reasoning or prior tool output. Results enter the main session untruncated and render as a compact, expandable `[context] researched` lifecycle row.
|
|
56
36
|
|
|
57
37
|
## Structure
|
|
58
38
|
|
|
59
|
-
```
|
|
39
|
+
```text
|
|
60
40
|
context/
|
|
61
|
-
├── index.ts
|
|
62
|
-
├── extensions/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
├──
|
|
66
|
-
|
|
67
|
-
├── agents/
|
|
68
|
-
│ └── context-researcher.md # Isolated research brief (manual)
|
|
69
|
-
└── README.md
|
|
41
|
+
├── index.ts
|
|
42
|
+
├── extensions/context-tools.ts
|
|
43
|
+
├── extensions/context-command.ts
|
|
44
|
+
├── references/workflow.md
|
|
45
|
+
├── extensions/context-prompt.ts
|
|
46
|
+
└── prompts/context-research.md
|
|
70
47
|
```
|
|
71
48
|
|
|
72
|
-
## Prerequisites
|
|
73
|
-
|
|
74
|
-
- Pi with extensions + custom tools
|
|
75
|
-
- Network access for external lookups
|
|
76
|
-
- `EXA_API_KEY` for the Exa method (optional for the other four)
|
|
77
|
-
|
|
78
|
-
## Best Practices
|
|
79
|
-
|
|
80
|
-
- **Local first**: check local context (package.json, imports) before external lookups.
|
|
81
|
-
- **Combine methods**: DeepWiki / clone for architecture, Context7 for API surface, Exa / web for community patterns.
|
|
82
|
-
- **Verify sources**: cross-reference fetched content against actual code.
|
|
83
|
-
- **Clean up**: remove `/tmp` clones when finished.
|
|
84
|
-
|
|
85
49
|
## License
|
|
86
50
|
|
|
87
51
|
MIT
|
|
88
|
-
|
|
89
|
-
## Author
|
|
90
|
-
|
|
91
|
-
Frad LEE (fradser@gmail.com)
|
|
@@ -1,121 +1,15 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
|
|
6
|
-
const PKG_DIR =
|
|
7
|
-
typeof __dirname === "string"
|
|
8
|
-
? __dirname
|
|
9
|
-
: dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
|
|
11
|
-
const WORKFLOW_PATH = join(PKG_DIR, "..", "references", "workflow.md");
|
|
12
|
-
|
|
13
|
-
const METHOD_FLAG = "--method=";
|
|
14
|
-
const ALL_METHODS = ["deepwiki", "context7", "exa", "clone", "web", "all"] as const;
|
|
15
|
-
const MANIFEST_FILES = ["package.json", "pyproject.toml", "go.mod", "Cargo.toml"];
|
|
16
|
-
|
|
17
3
|
const CONTEXT_GUIDANCE = `
|
|
18
|
-
##
|
|
4
|
+
## Isolated research
|
|
19
5
|
|
|
20
|
-
|
|
21
|
-
- Use \`context_context7\` for up-to-date library/API docs (optionally focused by topic).
|
|
22
|
-
- Use \`context_exa\` for web/code search when EXA_API_KEY is set; otherwise fall back to \`bash\` + \`curl\`.
|
|
23
|
-
- Use \`git clone --depth=1\` to /tmp plus \`read\`/\`bash\` for private repos or deep inspection; always clean up.
|
|
24
|
-
- Prefer small intermediate summaries over dumping raw API payloads into the conversation.
|
|
25
|
-
- The \`/context <targets...> [--method=...]\` command loads the full workflow when the user asks to research or understand code.
|
|
6
|
+
When the user asks to research, investigate, compare, or understand an external repository, library, codebase, or current technical topic, call \`context_get\`. It starts a separate Pi process with a read/bash allowlist, disabled extension discovery, and prompt-level no-modification guidance; research stays out of the main session. The child may inspect public repositories with a depth-1 temporary clone under /tmp.
|
|
26
7
|
`;
|
|
27
8
|
|
|
28
|
-
|
|
29
|
-
targets: string[];
|
|
30
|
-
methods: string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseArgs(raw: string): ParsedArgs {
|
|
34
|
-
const targets: string[] = [];
|
|
35
|
-
let methods: string[] = ["all"];
|
|
36
|
-
const tokens = raw.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
|
|
37
|
-
for (const token of tokens) {
|
|
38
|
-
if (token.startsWith(METHOD_FLAG)) {
|
|
39
|
-
methods = token
|
|
40
|
-
.slice(METHOD_FLAG.length)
|
|
41
|
-
.split(",")
|
|
42
|
-
.map((m) => m.trim().toLowerCase())
|
|
43
|
-
.filter((m) => (ALL_METHODS as readonly string[]).includes(m));
|
|
44
|
-
if (methods.length === 0) methods = ["all"];
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
targets.push(token.replace(/^"|"$/g, ""));
|
|
48
|
-
}
|
|
49
|
-
return { targets, methods };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function detectDependencies(cwd: string): string[] {
|
|
53
|
-
const deps = new Set<string>();
|
|
54
|
-
for (const file of MANIFEST_FILES) {
|
|
55
|
-
try {
|
|
56
|
-
const text = readFileSync(join(cwd, file), "utf8");
|
|
57
|
-
if (file === "package.json") {
|
|
58
|
-
const json = JSON.parse(text) as {
|
|
59
|
-
dependencies?: Record<string, string>;
|
|
60
|
-
devDependencies?: Record<string, string>;
|
|
61
|
-
};
|
|
62
|
-
for (const name of Object.keys(json.dependencies ?? {})) deps.add(name);
|
|
63
|
-
for (const name of Object.keys(json.devDependencies ?? {})) deps.add(name);
|
|
64
|
-
} else if (file === "pyproject.toml") {
|
|
65
|
-
for (const match of text.matchAll(/^\s*"([A-Za-z0-9_.-]+)"\s*[>=<~!]/gm)) {
|
|
66
|
-
deps.add(match[1]);
|
|
67
|
-
}
|
|
68
|
-
} else if (file === "go.mod") {
|
|
69
|
-
for (const match of text.matchAll(/^\s*([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\s+v/gm)) {
|
|
70
|
-
deps.add(match[1]);
|
|
71
|
-
}
|
|
72
|
-
} else if (file === "Cargo.toml") {
|
|
73
|
-
for (const match of text.matchAll(/^([A-Za-z0-9_-]+)\s*=\s*"[^"]+"/gm)) {
|
|
74
|
-
deps.add(match[1]);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// Missing or unreadable manifest — skip.
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return [...deps];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function loadWorkflow(): string {
|
|
85
|
-
return readFileSync(WORKFLOW_PATH, "utf8");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function buildUserPrompt(args: ParsedArgs, cwd: string): string {
|
|
89
|
-
const workflow = loadWorkflow();
|
|
90
|
-
const targets = args.targets.length > 0 ? args.targets : detectDependencies(cwd);
|
|
91
|
-
const header = [
|
|
92
|
-
"Run the /context workflow.",
|
|
93
|
-
"",
|
|
94
|
-
`Targets: ${targets.length > 0 ? targets.join(", ") : "(none provided and no dependencies detected in cwd)"}`,
|
|
95
|
-
`Allowed methods: ${args.methods.join(",")}`,
|
|
96
|
-
"",
|
|
97
|
-
"---",
|
|
98
|
-
"",
|
|
99
|
-
].join("\n");
|
|
100
|
-
return header + workflow;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function registerContextCommand(pi: ExtensionAPI): void {
|
|
9
|
+
export function registerContextGuidance(pi: ExtensionAPI): void {
|
|
104
10
|
pi.on("before_agent_start", async (event) => ({
|
|
105
11
|
systemPrompt: event.systemPrompt + CONTEXT_GUIDANCE,
|
|
106
12
|
}));
|
|
107
|
-
|
|
108
|
-
pi.registerCommand("context", {
|
|
109
|
-
description:
|
|
110
|
-
"Retrieve code context for repos, libraries, or a natural-language question via DeepWiki, Context7, Exa, clone, or web.",
|
|
111
|
-
handler: async (rawArgs, ctx) => {
|
|
112
|
-
const args = parseArgs(rawArgs.trim());
|
|
113
|
-
const prompt = buildUserPrompt(args, ctx.cwd);
|
|
114
|
-
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
13
|
}
|
|
118
14
|
|
|
119
|
-
export default
|
|
120
|
-
registerContextCommand(pi);
|
|
121
|
-
}
|
|
15
|
+
export default registerContextGuidance;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface ContextResearchPromptBindings {
|
|
4
|
+
userResearchRequest: string;
|
|
5
|
+
workingDirectory: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CONTEXT_REVIEW_COMPLETION = [
|
|
9
|
+
"Return one self-contained review of the requested context.",
|
|
10
|
+
"State the conclusion, cite concrete evidence, and distinguish observed facts from uncertainty.",
|
|
11
|
+
"Finish with a textual final answer rather than ending after a tool call.",
|
|
12
|
+
].join(" ");
|
|
13
|
+
|
|
14
|
+
const CONTEXT_RESEARCH_TEMPLATE_URL = new URL("../prompts/context-research.md", import.meta.url);
|
|
15
|
+
const PLACEHOLDER_PATTERN = /\{\{([A-Z][A-Z0-9_]*)\}\}/g;
|
|
16
|
+
const PLACEHOLDER_TOKEN_PATTERN = /\{\{[^{}]+\}\}/g;
|
|
17
|
+
const REQUIRED_PLACEHOLDERS = ["USER_RESEARCH_REQUEST"] as const;
|
|
18
|
+
|
|
19
|
+
function templatePlaceholders(template: string): string[] {
|
|
20
|
+
return [...template.matchAll(PLACEHOLDER_PATTERN)].map((match) => match[1]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function placeholderTokens(template: string): string[] {
|
|
24
|
+
return template.match(PLACEHOLDER_TOKEN_PATTERN) ?? [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validateContextPromptTemplate(template: string): void {
|
|
28
|
+
const placeholders = templatePlaceholders(template);
|
|
29
|
+
const allowedTokens = new Set(REQUIRED_PLACEHOLDERS.map((placeholder) => `{{${placeholder}}}`));
|
|
30
|
+
const unknown = [...new Set(placeholderTokens(template).filter((token) => !allowedTokens.has(token)))];
|
|
31
|
+
if (unknown.length > 0) {
|
|
32
|
+
throw new Error(`Context prompt has unknown placeholder(s): ${unknown.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
const missing = REQUIRED_PLACEHOLDERS.filter((placeholder) => !placeholders.includes(placeholder));
|
|
35
|
+
if (missing.length > 0) {
|
|
36
|
+
throw new Error(`Context prompt has missing placeholder(s): ${missing.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderContextPromptTemplate(
|
|
41
|
+
template: string,
|
|
42
|
+
bindings: ContextResearchPromptBindings,
|
|
43
|
+
): string {
|
|
44
|
+
validateContextPromptTemplate(template);
|
|
45
|
+
const rendered = template.replace(
|
|
46
|
+
"{{USER_RESEARCH_REQUEST}}",
|
|
47
|
+
() => bindings.userResearchRequest,
|
|
48
|
+
);
|
|
49
|
+
const bindingTokens = new Set(placeholderTokens(bindings.userResearchRequest));
|
|
50
|
+
const unexpected = placeholderTokens(rendered).filter((token) => !bindingTokens.has(token));
|
|
51
|
+
if (unexpected.length > 0) {
|
|
52
|
+
throw new Error(`Context prompt has unresolved placeholder(s): ${[...new Set(unexpected)].join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
return rendered;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const CONTEXT_RESEARCH_TEMPLATE = readFileSync(CONTEXT_RESEARCH_TEMPLATE_URL, "utf8").trim();
|
|
58
|
+
|
|
59
|
+
export function buildContextResearchPrompt(bindings: ContextResearchPromptBindings): string {
|
|
60
|
+
const protocol = renderContextPromptTemplate(CONTEXT_RESEARCH_TEMPLATE, bindings);
|
|
61
|
+
return [
|
|
62
|
+
"# Context Review",
|
|
63
|
+
"",
|
|
64
|
+
"## Current Research Task",
|
|
65
|
+
`Working directory: ${bindings.workingDirectory}`,
|
|
66
|
+
"",
|
|
67
|
+
bindings.userResearchRequest,
|
|
68
|
+
"",
|
|
69
|
+
"## Reference Protocol",
|
|
70
|
+
protocol,
|
|
71
|
+
"",
|
|
72
|
+
"## Completion Contract",
|
|
73
|
+
CONTEXT_REVIEW_COMPLETION,
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
@@ -1,277 +1,231 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { truncateHead, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
1
|
+
import { getMarkdownTheme, keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, Markdown, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
createLiveActivityWidget,
|
|
5
|
+
createStaticToolLifecycleResultRenderer,
|
|
6
|
+
eventToolLifecycle,
|
|
7
|
+
runPiWorker,
|
|
8
|
+
type PiLiveWidgetContext,
|
|
9
|
+
type PiWorkerProgressUpdate,
|
|
10
|
+
} from "@fradser/pi-kit";
|
|
16
11
|
import { Type } from "typebox";
|
|
12
|
+
import { buildContextResearchPrompt } from "./context-prompt.ts";
|
|
13
|
+
|
|
14
|
+
const RESEARCH_TOOLS = ["read", "bash"];
|
|
15
|
+
const FINAL_ANSWER_RETRY_INSTRUCTION = `
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
const TIMEOUT_MS = 30_000;
|
|
20
|
-
const DEEPWIKI_MCP = "https://mcp.deepwiki.com/mcp";
|
|
17
|
+
Completion requirement: the previous attempt produced no textual final answer. Complete the original research request now without relying on hidden reasoning, transient tool output, or state from the prior attempt; repeat any inspection needed to support the answer. Your final assistant message must be a self-contained plain-text answer with the conclusion and concrete evidence. Do not finish immediately after a tool call.`;
|
|
21
18
|
|
|
22
19
|
interface ToolTextResult {
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
content: [{ type: "text"; text: string }];
|
|
21
|
+
details: Record<string, unknown>;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
interface ChildResult {
|
|
25
|
+
text: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
exitCode: number;
|
|
28
|
+
cancelled: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const result = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: MAX_CHARS });
|
|
34
|
-
return `${result.content}\n\n…[truncated ${text.length - MAX_CHARS} chars]`;
|
|
31
|
+
interface ResearchAttemptResult extends ChildResult {
|
|
32
|
+
retried: boolean;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
type ResearchRunner = (
|
|
36
|
+
query: string,
|
|
37
|
+
signal: AbortSignal | undefined,
|
|
38
|
+
onUpdate: ((update: PiWorkerProgressUpdate) => void) | undefined,
|
|
39
|
+
finalAnswerRetry: boolean,
|
|
40
|
+
) => Promise<ChildResult>;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
init: RequestInit = {},
|
|
45
|
-
signal: AbortSignal | undefined,
|
|
46
|
-
): Promise<{ ok: boolean; status: number; body: string }> {
|
|
47
|
-
const res = await fetch(url, { ...init, signal: requestSignal(signal) });
|
|
48
|
-
const body = await res.text();
|
|
49
|
-
return { ok: res.ok, status: res.status, body };
|
|
42
|
+
function textResult(text: string, details: Record<string, unknown> = {}): ToolTextResult {
|
|
43
|
+
return { content: [{ type: "text", text }], details };
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*/
|
|
56
|
-
async function deepwikiCall(
|
|
57
|
-
tool: string,
|
|
58
|
-
args: Record<string, unknown>,
|
|
59
|
-
signal: AbortSignal | undefined,
|
|
60
|
-
): Promise<string> {
|
|
61
|
-
const res = await fetch(DEEPWIKI_MCP, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: {
|
|
64
|
-
"Content-Type": "application/json",
|
|
65
|
-
Accept: "application/json, text/event-stream",
|
|
66
|
-
},
|
|
67
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: tool, arguments: args } }),
|
|
68
|
-
signal: requestSignal(signal),
|
|
69
|
-
});
|
|
70
|
-
const body = await res.text();
|
|
71
|
-
if (!res.ok) throw new Error(`DeepWiki HTTP ${res.status}: ${body.slice(0, 200)}`);
|
|
72
|
-
|
|
73
|
-
// SSE frames: `event: message` + one or more `data:` lines, blank-line terminated.
|
|
74
|
-
// Join multiline payloads, keep the last frame with data.
|
|
75
|
-
let payload: string | null = null;
|
|
76
|
-
let currentData: string[] = [];
|
|
77
|
-
for (const raw of body.split("\n")) {
|
|
78
|
-
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
79
|
-
if (line === "") {
|
|
80
|
-
if (currentData.length > 0) payload = currentData.join("\n");
|
|
81
|
-
currentData = [];
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (line.startsWith(":")) continue; // SSE comment
|
|
85
|
-
if (line.startsWith("data:")) {
|
|
86
|
-
currentData.push(line.slice(5).replace(/^ /, ""));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (currentData.length > 0) payload = currentData.join("\n");
|
|
90
|
-
if (payload === null) throw new Error("DeepWiki: no data frame in response");
|
|
91
|
-
|
|
92
|
-
const data = JSON.parse(payload);
|
|
93
|
-
if (data.error) throw new Error(`DeepWiki: ${data.error.message ?? JSON.stringify(data.error)}`);
|
|
94
|
-
const content = data.result?.content;
|
|
95
|
-
if (Array.isArray(content)) {
|
|
96
|
-
return content.map((c) => (typeof c?.text === "string" ? c.text : JSON.stringify(c))).join("\n");
|
|
97
|
-
}
|
|
98
|
-
return JSON.stringify(data.result ?? data);
|
|
46
|
+
export function formatResearchSubject(query: unknown): string {
|
|
47
|
+
if (typeof query !== "string") return "context request";
|
|
48
|
+
return query.replace(/\s+/g, " ").trim() || "context request";
|
|
99
49
|
}
|
|
100
50
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
51
|
+
function renderContextCall(
|
|
52
|
+
query: unknown,
|
|
53
|
+
theme: { fg(color: string, text: string): string; bold(text: string): string },
|
|
54
|
+
): Component {
|
|
55
|
+
return {
|
|
56
|
+
render: (width) => {
|
|
57
|
+
if (width <= 0) return [];
|
|
58
|
+
const prefix = theme.fg("customMessageLabel", theme.bold("[context]"));
|
|
59
|
+
const title = `research started · ${formatResearchSubject(query)}`;
|
|
60
|
+
return new Text(`${prefix} ${title}`, 0, 0).render(width)
|
|
61
|
+
.map((line) => truncateToWidth(line, width, "")).concat("");
|
|
62
|
+
},
|
|
63
|
+
invalidate: () => {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
117
66
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Type.Number({ description: "Number of results (default 5)", default: 5 }),
|
|
122
|
-
),
|
|
123
|
-
});
|
|
67
|
+
function resultText(result: ToolTextResult): string {
|
|
68
|
+
return result.content.find((part) => part.type === "text")?.text ?? "";
|
|
69
|
+
}
|
|
124
70
|
|
|
125
|
-
export function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"mode=ask answers a specific question with sources. No API key required.",
|
|
133
|
-
promptSnippet: "Look up AI-generated documentation for a public GitHub repo from DeepWiki",
|
|
134
|
-
promptGuidelines: [
|
|
135
|
-
"Use context_deepwiki to fetch structured repo documentation via DeepWiki instead of cloning when you only need a high-level understanding.",
|
|
136
|
-
],
|
|
137
|
-
parameters: DeepWikiParams,
|
|
138
|
-
executionMode: "sequential",
|
|
71
|
+
export function buildResearchPrompt(query: string, finalAnswerRetry = false): string {
|
|
72
|
+
const prompt = buildContextResearchPrompt({
|
|
73
|
+
userResearchRequest: query,
|
|
74
|
+
workingDirectory: process.cwd(),
|
|
75
|
+
});
|
|
76
|
+
return finalAnswerRetry ? `${prompt}${FINAL_ANSWER_RETRY_INSTRUCTION}` : prompt;
|
|
77
|
+
}
|
|
139
78
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (params.mode === "ask") {
|
|
143
|
-
if (!params.question) {
|
|
144
|
-
return textResult("context_deepwiki: mode 'ask' requires a question");
|
|
145
|
-
}
|
|
146
|
-
const answer = await deepwikiCall("ask_question", { repoName: repo, question: params.question }, signal);
|
|
147
|
-
return textResult(truncate(answer));
|
|
148
|
-
}
|
|
149
|
-
const tool = params.mode === "structure" ? "read_wiki_structure" : "read_wiki_contents";
|
|
150
|
-
const out = await deepwikiCall(tool, { repoName: repo }, signal);
|
|
151
|
-
return textResult(truncate(out));
|
|
152
|
-
},
|
|
153
|
-
});
|
|
79
|
+
/** Minimal structural view of the tool-execution context: only what the research widget needs. */
|
|
80
|
+
export type ResearchWidgetContext = PiLiveWidgetContext;
|
|
154
81
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"Searches the library, then retrieves markdown docs (optionally focused on a topic). " +
|
|
161
|
-
"Anonymous usage is rate-limited; set CONTEXT7_API_KEY (Authorization: Bearer) for per-key quota.",
|
|
162
|
-
promptSnippet: "Look up version-aware library documentation from Context7",
|
|
163
|
-
promptGuidelines: [
|
|
164
|
-
"Use context_context7 to look up library docs instead of guessing APIs from memory. Anonymous usage is rate-limited; set CONTEXT7_API_KEY for higher quota.",
|
|
165
|
-
],
|
|
166
|
-
parameters: Context7Params,
|
|
167
|
-
executionMode: "sequential",
|
|
82
|
+
interface ActiveResearch {
|
|
83
|
+
token: number;
|
|
84
|
+
ctx: ResearchWidgetContext | undefined;
|
|
85
|
+
activity?: string;
|
|
86
|
+
}
|
|
168
87
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
88
|
+
let researchToken = 0;
|
|
89
|
+
let activeResearch: ActiveResearch | undefined;
|
|
90
|
+
const researchWidget = createLiveActivityWidget({
|
|
91
|
+
key: "context-research",
|
|
92
|
+
placement: "aboveEditor",
|
|
93
|
+
fit: truncateToWidth,
|
|
94
|
+
formatIdentity: (identity, theme) => theme.fg("success", theme.bold(identity)),
|
|
95
|
+
formatActivity: (activity, theme) => new Markdown(
|
|
96
|
+
activity, 0, 0, getMarkdownTheme(), { color: (text) => theme.fg("accent", text) },
|
|
97
|
+
).render(Math.max(1, visibleWidth(activity))).map((line) => line.trim()).filter(Boolean).join(" "),
|
|
98
|
+
});
|
|
174
99
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return textResult(
|
|
183
|
-
`Context7 search needs an API key (HTTP ${status}). Set CONTEXT7_API_KEY ` +
|
|
184
|
-
`(Authorization: Bearer) or fall back to web/clone methods.`,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
throw new Error(`Context7 search failed (HTTP ${status}): ${body.slice(0, 400)}`);
|
|
188
|
-
}
|
|
100
|
+
/** Show the running-research status above the editor; returns a token for updates/clear. */
|
|
101
|
+
export function startResearchWidget(ctx: ResearchWidgetContext | undefined): number {
|
|
102
|
+
const token = ++researchToken;
|
|
103
|
+
activeResearch = { token, ctx };
|
|
104
|
+
researchWidget.update(ctx, [{ id: String(token), identity: "researcher" }]);
|
|
105
|
+
return token;
|
|
106
|
+
}
|
|
189
107
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const libraryId = typeof results[0]?.id === "string" ? results[0].id : "";
|
|
201
|
-
if (!libraryId) {
|
|
202
|
-
return textResult(`Context7: search returned no usable library id for "${params.query}"`);
|
|
203
|
-
}
|
|
108
|
+
/** Refresh the live activity line; stale tokens are ignored. */
|
|
109
|
+
export function updateResearchWidget(token: number, activity: string | undefined): void {
|
|
110
|
+
if (activeResearch?.token !== token) return;
|
|
111
|
+
if (activity) activeResearch.activity = activity;
|
|
112
|
+
researchWidget.update(activeResearch.ctx, [{
|
|
113
|
+
id: String(token),
|
|
114
|
+
identity: "researcher",
|
|
115
|
+
activity: activeResearch.activity,
|
|
116
|
+
}]);
|
|
117
|
+
}
|
|
204
118
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
throw new Error(`Context7 docs failed (HTTP ${doc.status}): ${doc.body.slice(0, 400)}`);
|
|
212
|
-
}
|
|
213
|
-
return textResult(
|
|
214
|
-
`Context7 docs for "${params.query}" (${libraryId})${params.topic ? `, topic ${params.topic}` : ""}:\n\n${truncate(doc.body)}`,
|
|
215
|
-
);
|
|
216
|
-
},
|
|
217
|
-
});
|
|
119
|
+
/** Remove the status widget; stale tokens are ignored. */
|
|
120
|
+
export function clearResearchWidget(ctx: ResearchWidgetContext | undefined, token: number): void {
|
|
121
|
+
if (activeResearch?.token !== token) return;
|
|
122
|
+
activeResearch = undefined;
|
|
123
|
+
researchWidget.clear(ctx);
|
|
124
|
+
}
|
|
218
125
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
126
|
+
export function runResearchChild(
|
|
127
|
+
query: string,
|
|
128
|
+
signal?: AbortSignal,
|
|
129
|
+
onUpdate?: (update: PiWorkerProgressUpdate) => void,
|
|
130
|
+
finalAnswerRetry = false,
|
|
131
|
+
): Promise<ChildResult> {
|
|
132
|
+
let cancelled = signal?.aborted ?? false;
|
|
133
|
+
const onAbort = () => { cancelled = true; };
|
|
134
|
+
if (!signal?.aborted) signal?.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
return runPiWorker({
|
|
136
|
+
prompt: buildResearchPrompt(query, finalAnswerRetry),
|
|
137
|
+
cwd: process.cwd(),
|
|
138
|
+
tools: RESEARCH_TOOLS,
|
|
139
|
+
minimal: true,
|
|
140
|
+
signal,
|
|
141
|
+
onUpdate,
|
|
142
|
+
}).then((result) => {
|
|
143
|
+
signal?.removeEventListener("abort", onAbort);
|
|
144
|
+
return { text: result.text.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode, cancelled };
|
|
145
|
+
});
|
|
146
|
+
}
|
|
231
147
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
148
|
+
export async function runResearchWithFinalAnswerRetry(
|
|
149
|
+
query: string,
|
|
150
|
+
signal?: AbortSignal,
|
|
151
|
+
onUpdate?: (update: PiWorkerProgressUpdate) => void,
|
|
152
|
+
runner: ResearchRunner = runResearchChild,
|
|
153
|
+
): Promise<ResearchAttemptResult> {
|
|
154
|
+
const first = await runner(query, signal, onUpdate, false);
|
|
155
|
+
if (first.cancelled || first.exitCode !== 0 || first.text) return { ...first, retried: false };
|
|
156
|
+
const retry = await runner(query, signal, onUpdate, true);
|
|
157
|
+
return { ...retry, retried: true };
|
|
158
|
+
}
|
|
240
159
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
{
|
|
245
|
-
method: "POST",
|
|
246
|
-
headers: {
|
|
247
|
-
"Content-Type": "application/json",
|
|
248
|
-
"x-api-key": apiKey,
|
|
249
|
-
},
|
|
250
|
-
body: JSON.stringify({ query: params.query, numResults, contents: { text: true } }),
|
|
251
|
-
},
|
|
252
|
-
signal,
|
|
253
|
-
);
|
|
254
|
-
if (!ok) throw new Error(`Exa search failed (HTTP ${status}): ${body.slice(0, 400)}`);
|
|
160
|
+
const ResearchParams = Type.Object({
|
|
161
|
+
query: Type.String({ description: "Research request: repository, library, codebase question, or current technical topic." }),
|
|
162
|
+
});
|
|
255
163
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
164
|
+
export function registerContextTools(pi: ExtensionAPI): void {
|
|
165
|
+
pi.registerTool({
|
|
166
|
+
name: "context_get",
|
|
167
|
+
label: "Isolated Pi research",
|
|
168
|
+
description: "Research a repository, library, codebase, or technical topic in an independent prompt-constrained Pi child process. The child has read and bash available and can inspect or temporarily clone public repositories under /tmp.",
|
|
169
|
+
promptSnippet: "Research independently in a prompt-constrained Pi child process",
|
|
170
|
+
promptGuidelines: [
|
|
171
|
+
"Use context_get when the user needs external context for a repository, library, codebase, or current technical topic. It retrieves that context in an isolated Pi process with a read/bash allowlist and prompt-level no-modification guidance.",
|
|
172
|
+
],
|
|
173
|
+
parameters: ResearchParams,
|
|
174
|
+
executionMode: "sequential",
|
|
175
|
+
renderShell: "self",
|
|
176
|
+
renderCall(args, theme, _context) {
|
|
177
|
+
return renderContextCall(args.query, theme);
|
|
178
|
+
},
|
|
179
|
+
renderResult(result, options, theme, context) {
|
|
180
|
+
// While the async child is still running, live progress already shows
|
|
181
|
+
// in the `researcher` widget above the editor, so a partial result
|
|
182
|
+
// renders no transcript row: the `research started` call row stays the
|
|
183
|
+
// single visible row until the settled `[context] researched` result.
|
|
184
|
+
if (options.isPartial) {
|
|
185
|
+
return { render: () => [] as string[], invalidate: () => {} };
|
|
186
|
+
}
|
|
187
|
+
const query = (context.args as { query?: string })?.query;
|
|
188
|
+
const subject = formatResearchSubject(query);
|
|
189
|
+
const text = resultText(result as ToolTextResult);
|
|
190
|
+
const details = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
191
|
+
const spec = eventToolLifecycle("context", subject, {
|
|
192
|
+
label: "researched",
|
|
193
|
+
details,
|
|
194
|
+
detailLimit: "all",
|
|
195
|
+
});
|
|
196
|
+
return createStaticToolLifecycleResultRenderer({
|
|
197
|
+
createSpec: () => spec,
|
|
198
|
+
expandHint: keyHint("app.tools.expand", "to expand"),
|
|
199
|
+
fit: truncateToWidth,
|
|
200
|
+
visibleWidth,
|
|
201
|
+
wrapDetail: (line, width) => wrapTextWithAnsi(line, Math.max(1, width)),
|
|
202
|
+
})(result as ToolTextResult, options, theme, context);
|
|
203
|
+
},
|
|
204
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
205
|
+
const widget = startResearchWidget(ctx as ResearchWidgetContext | undefined);
|
|
206
|
+
try {
|
|
207
|
+
const child = await runResearchWithFinalAnswerRetry(params.query, signal, (progress) => {
|
|
208
|
+
updateResearchWidget(widget, progress.activity);
|
|
209
|
+
onUpdate?.(textResult(progress.activity ?? "Working...", {
|
|
210
|
+
operation: "context-review",
|
|
211
|
+
}));
|
|
212
|
+
});
|
|
213
|
+
if (child.cancelled) throw new Error("Isolated Pi research was cancelled");
|
|
214
|
+
if (child.exitCode !== 0) {
|
|
215
|
+
const phase = child.retried ? " retry" : "";
|
|
216
|
+
throw new Error(`Isolated Pi research${phase} failed (exit ${child.exitCode}): ${child.stderr.slice(0, 400)}`);
|
|
217
|
+
}
|
|
218
|
+
if (!child.text) throw new Error("Isolated Pi research returned no answer after retry");
|
|
219
|
+
return textResult(child.text, {
|
|
220
|
+
exitCode: child.exitCode,
|
|
221
|
+
operation: "context-review",
|
|
222
|
+
...(child.retried ? { retried: true } : {}),
|
|
223
|
+
});
|
|
224
|
+
} finally {
|
|
225
|
+
clearResearchWidget(ctx as ResearchWidgetContext | undefined, widget);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
});
|
|
275
229
|
}
|
|
276
230
|
|
|
277
231
|
export default registerContextTools;
|
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import registerContextGuidance from "./extensions/context-command.ts";
|
|
3
3
|
import registerContextTools from "./extensions/context-tools.ts";
|
|
4
4
|
|
|
5
5
|
export default function contextExtension(pi: ExtensionAPI): void {
|
|
6
6
|
registerContextTools(pi);
|
|
7
|
-
|
|
7
|
+
registerContextGuidance(pi);
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fradser/pi-context",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Retrieve
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Retrieve context for repositories, libraries, and technical questions with one native tool that launches an isolated prompt-constrained Pi research child.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"code-context",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"exa"
|
|
8
|
+
"isolated-research",
|
|
9
|
+
"prompt-constrained"
|
|
11
10
|
],
|
|
12
11
|
"author": "Frad LEE <fradser@gmail.com>",
|
|
13
12
|
"license": "MIT",
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
"index.ts",
|
|
23
22
|
"extensions",
|
|
24
23
|
"references",
|
|
25
|
-
"
|
|
24
|
+
"prompts",
|
|
26
25
|
"README.md"
|
|
27
26
|
],
|
|
28
27
|
"pi": {
|
|
@@ -31,8 +30,12 @@
|
|
|
31
30
|
]
|
|
32
31
|
},
|
|
33
32
|
"peerDependencies": {
|
|
34
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
35
33
|
"@earendil-works/pi-ai": "*",
|
|
34
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
35
|
+
"@earendil-works/pi-tui": "*",
|
|
36
36
|
"typebox": "*"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@fradser/pi-kit": "0.4.2"
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
You are the stateless one-shot research worker bundled with @fradser/pi-context and invoked by its context_get tool.
|
|
2
|
+
|
|
3
|
+
Research the supplied request independently and return a concise, evidence-based answer. You have read and bash available. Treat the caller's working directory as read-only: never edit or write files. Bash can technically write, so this boundary is enforced by your instructions rather than an OS sandbox.
|
|
4
|
+
|
|
5
|
+
For public repository line-level evidence, you may run git clone --depth=1 into a unique /tmp directory, inspect it, and remove it before answering. Never modify the caller's working directory. Do not use package managers, deployment commands, or interactive commands.
|
|
6
|
+
|
|
7
|
+
Cite concrete source URLs, repository paths, or documentation names when available. Keep raw findings compact and synthesize the answer rather than dumping large payloads.
|
|
8
|
+
|
|
9
|
+
User research request:
|
|
10
|
+
{{USER_RESEARCH_REQUEST}}
|
package/references/workflow.md
CHANGED
|
@@ -1,108 +1,7 @@
|
|
|
1
|
-
# context workflow
|
|
1
|
+
# Isolated context research workflow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `context_get` for a repository, library, codebase, or technical research request. It launches a stateless one-shot Pi worker so exploratory work and its tool calls do not enter the main session.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A package-local typed builder reads the bundled Markdown as a reference protocol and constructs a task-specific context-review prompt from the current request and working directory, then adds an explicit completion contract. The transcript identifies that execution with the normalized concrete request (`[context] research started · <research query>`), never with the Markdown path; while the async child runs, no duplicate progress row is rendered and live progress shows only in the researcher widget. The child has a `read,bash` allowlist and no extension discovery. The prompt requires read-only behavior and forbids edits, writes, package-manager commands, deployment, and interactive commands; `bash` can technically write because the child has no OS sandbox. For public repository evidence it may run `git clone --depth=1` into a unique directory under `/tmp`, inspect only the relevant files, cite the evidence in its answer, and remove the clone before it exits.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|--------|------|--------------|
|
|
9
|
-
| DeepWiki | `context_deepwiki` | always, no key |
|
|
10
|
-
| Context7 | `context_context7` | always, optional `CONTEXT7_API_KEY` for higher quota |
|
|
11
|
-
| Exa | `context_exa` | requires `EXA_API_KEY` |
|
|
12
|
-
| Git clone | `bash` + `read` | always |
|
|
13
|
-
| Web search+fetch | `bash` + `curl` | always |
|
|
14
|
-
|
|
15
|
-
If a native tool is unavailable or errors (e.g. `EXA_API_KEY` unset), skip it and use the documented clone/web fallback. Report which methods ran. There are no Pi built-ins named `WebSearch`/`WebFetch`/`Task` — do not invent them.
|
|
16
|
-
|
|
17
|
-
## Invocation
|
|
18
|
-
|
|
19
|
-
`/context [targets...] [--method=a,b,...]`
|
|
20
|
-
|
|
21
|
-
- `--method=` accepts a comma-separated list from `deepwiki,context7,exa,clone,web,all`. Default: `all`.
|
|
22
|
-
- Quoted strings are one target. Multiple positional tokens are multiple targets.
|
|
23
|
-
- No targets: read dependency manifests in cwd (`package.json`, `go.mod`, `pyproject.toml`, `Cargo.toml`) and use detected dependencies as targets.
|
|
24
|
-
|
|
25
|
-
Classify each target:
|
|
26
|
-
|
|
27
|
-
- GitHub slug (`owner/repo`) or git URL → repo — DeepWiki (`context_deepwiki`), else clone.
|
|
28
|
-
- Bare package name or `name@version` → library — Context7 (`context_context7`), else web + clone of docs repo when known.
|
|
29
|
-
- Anything else → natural-language — Exa (`context_exa`) when `EXA_API_KEY` is set, else web search via `curl`.
|
|
30
|
-
|
|
31
|
-
When `--method=` is passed, use the intersection of allowed and applicable methods. Empty intersection → skip and report.
|
|
32
|
-
|
|
33
|
-
## Method 1: DeepWiki
|
|
34
|
-
|
|
35
|
-
Best for: public GitHub repos needing an architecture overview quickly.
|
|
36
|
-
|
|
37
|
-
1. `context_deepwiki` with `owner`/`repo` and `mode: "structure"` (e.g. `facebook` / `react`).
|
|
38
|
-
2. `mode: "contents"` for the full wiki text, or `mode: "ask"` with `question` for targeted Q&A.
|
|
39
|
-
3. Extract architecture summary and key relationships.
|
|
40
|
-
|
|
41
|
-
Token isolation: structure → relevant pages → short architecture summary.
|
|
42
|
-
Fallback on error: Method 4 (clone) + Method 5 (web) on GitHub docs/README.
|
|
43
|
-
|
|
44
|
-
## Method 2: Context7
|
|
45
|
-
|
|
46
|
-
Best for: up-to-date API docs and version-specific examples for packages/frameworks.
|
|
47
|
-
|
|
48
|
-
1. `context_context7` with the library name as `query` (e.g. `fastapi`, `react`).
|
|
49
|
-
2. Pass a `topic` (e.g. `auth`, `ssr`) for focused snippets; include the version in the query when given (`react@18`).
|
|
50
|
-
3. Extract the minimum viable API surface + examples.
|
|
51
|
-
|
|
52
|
-
Token isolation: resolve id → query → minimum viable API surface + examples.
|
|
53
|
-
Fallback on error: official docs URLs via `curl`, or clone of the library repo docs.
|
|
54
|
-
|
|
55
|
-
## Method 3: Exa
|
|
56
|
-
|
|
57
|
-
Best for: real-world usage patterns and cross-web code examples.
|
|
58
|
-
|
|
59
|
-
1. Call with a precise query (language/framework/version/identifiers).
|
|
60
|
-
2. Prefer recent sources; dedupe near-identical snippets and mirrors.
|
|
61
|
-
3. Return a short explanation with sources.
|
|
62
|
-
|
|
63
|
-
Fallback when key missing / error: GitHub code search URLs / known docs via `curl`, or clone representative examples.
|
|
64
|
-
|
|
65
|
-
## Method 4: Git clone
|
|
66
|
-
|
|
67
|
-
Best for: private repos, deep implementation review, when other methods lack depth.
|
|
68
|
-
|
|
69
|
-
1. `git clone <repo-url> /tmp/<repo-name> --depth=1`
|
|
70
|
-
2. `read` key files: entry points, config, core modules.
|
|
71
|
-
3. Map structure with `bash` find/ls; search with `bash` grep.
|
|
72
|
-
4. Cleanup: `rm -rf /tmp/<repo-name>`.
|
|
73
|
-
|
|
74
|
-
## Method 5: Web search + fetch
|
|
75
|
-
|
|
76
|
-
Best for: rationale, changelogs, issues, migration guides.
|
|
77
|
-
|
|
78
|
-
1. Derive version-anchored queries from the question or clone findings.
|
|
79
|
-
2. Fetch high-signal URLs (official docs, GitHub issues/PRs, changelogs).
|
|
80
|
-
3. Extract only relevant sections; cross-check against cloned code when available.
|
|
81
|
-
4. Discard results older than ~2 years unless foundational.
|
|
82
|
-
|
|
83
|
-
Query patterns:
|
|
84
|
-
- Changelogs: `"<repo> CHANGELOG v<version>"`
|
|
85
|
-
- Rationale: `"<repo> <concept> why OR rationale site:github.com"`
|
|
86
|
-
- Known issues: `"<repo> <symbol> issue OR bug site:github.com"`
|
|
87
|
-
- Migration: `"<repo> migrate from <old> to <new>"`
|
|
88
|
-
|
|
89
|
-
## Method selection guide
|
|
90
|
-
|
|
91
|
-
| Scenario | Primary | Fallback |
|
|
92
|
-
|----------|---------|----------|
|
|
93
|
-
| "How does X library work?" | Context7 | Web / clone |
|
|
94
|
-
| "Architecture of Y repo" | DeepWiki | Git clone |
|
|
95
|
-
| "Examples of Z pattern" | Exa | Context7 / web |
|
|
96
|
-
| "Private/internal repo" | Git clone | — |
|
|
97
|
-
| "What changed in v3?" | Context7 | Web |
|
|
98
|
-
| "Why this design?" | Git clone → web | DeepWiki |
|
|
99
|
-
| "Compare X vs Y" | Exa + Context7 | Web |
|
|
100
|
-
| "Best practice for Z" | Web | Exa |
|
|
101
|
-
|
|
102
|
-
## Combining methods
|
|
103
|
-
|
|
104
|
-
1. DeepWiki / clone for architecture.
|
|
105
|
-
2. Context7 for API surface.
|
|
106
|
-
3. Exa / web for community patterns.
|
|
107
|
-
4. Always prefer read-only operations; clean up `/tmp` clones.
|
|
108
|
-
5. Return a synthesized summary only — drop raw bulk payloads.
|
|
7
|
+
The returned answer should synthesize findings, distinguish facts from uncertainty, and cite useful source URLs or repository paths. It must not modify the caller's working directory.
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: context-researcher
|
|
3
|
-
description: Research a library, repository, or code pattern without polluting the main conversation. Uses the native context_deepwiki/context_context7/context_exa tools when applicable, otherwise git clone + HTTP fetch via Pi bash/read. Accepts natural-language queries, repo slugs, library names, or any combination.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Context researcher (optional agent brief)
|
|
7
|
-
|
|
8
|
-
Pi does not load Claude Code agent files automatically. Treat this file as a **prompt brief** when you want an isolated research pass. Prefer the `/context` command for the executable workflow.
|
|
9
|
-
|
|
10
|
-
## Runtime requirements
|
|
11
|
-
|
|
12
|
-
| Method | Requirement |
|
|
13
|
-
|--------|-------------|
|
|
14
|
-
| DeepWiki | Native `context_deepwiki` tool |
|
|
15
|
-
| Context7 | Native `context_context7` tool |
|
|
16
|
-
| Exa | Native `context_exa` tool (+ optional `EXA_API_KEY`) |
|
|
17
|
-
| Git clone | `git` + Pi `bash` / `read` (always) |
|
|
18
|
-
| Web | `curl` or host HTTP via Pi `bash` (always) |
|
|
19
|
-
|
|
20
|
-
If a native context tool is unavailable or fails, skip that method, use clone + web, and report which methods ran and why the fallback was needed.
|
|
21
|
-
|
|
22
|
-
## Process
|
|
23
|
-
|
|
24
|
-
1. **Parse the request** — targets and optional `--method=` list (see `/context`).
|
|
25
|
-
2. **Classify** each target (repo / library / natural-language).
|
|
26
|
-
3. **Select methods** per the `/context` workflow's selection guide.
|
|
27
|
-
4. **Execute with Pi tools** — use the applicable native context tools (`context_deepwiki`, `context_context7`, `context_exa`) plus `bash` and `read`. Summarize intermediates; do not dump raw payloads.
|
|
28
|
-
5. **Return** a concise architecture/API/examples summary only.
|
|
29
|
-
|
|
30
|
-
## Output format
|
|
31
|
-
|
|
32
|
-
- Target classification
|
|
33
|
-
- Methods used (and skipped, with reason)
|
|
34
|
-
- Key findings (bullets)
|
|
35
|
-
- Copyable snippets only when necessary
|
|
36
|
-
- Open questions / confidence notes
|