@thurstonsand/pi-librarian 0.2.1 → 0.3.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 CHANGED
@@ -8,14 +8,21 @@ A GitHub research subagent for the [pi coding agent](https://github.com/badlogic
8
8
 
9
9
  ## How it works
10
10
 
11
- The `librarian` tool spawns a nested research agent with purpose-built tools:
11
+ The `librarian` tool spawns a research subagent with purpose-built tools:
12
12
 
13
- - **`checkout_repo`** — clone into a local cache; pi's own `grep`/`read`/`find` then work on real files, and `git log -S`/`blame`/`diff` cover history.
14
- - **`search_repos`** — GitHub repository discovery (stars, topics, language).
13
+ - **`checkout_repo`** — clone into a local cache, where it can be read locally, and `git log -S`/`blame`/`diff` covers history.
14
+ - **`search_repos`** — GitHub repository discovery (stars, topics, other metadata).
15
15
  - **`search_code`** — cross-repo public code search via [Grep](https://grep.app/) (regex, global discovery, repo/language/path filters).
16
16
  - **`search_github_code`** — GitHub REST code search over public code and private repositories your configured GitHub auth can access.
17
17
  - **`read_github_file`** — single-file API reads for quick peeks without cloning.
18
18
 
19
+ ## Usage
20
+
21
+ - Ask pi a question involving other repos; it asks the `librarian`.
22
+ - Ask follow-up questions to earlier librarian runs.
23
+ - `/librarian` attaches the research tools directly to your session for direct tool usage.
24
+ - Recommended: install [pi-web-access](https://pi.dev/packages/pi-web-access) (or your web search of choice, tho this one is zero-config to get started) and add `web_search`, `fetch_content`, `get_search_content` to `librarian.tools` to expand past GitHub-only research.
25
+
19
26
  ## GitHub auth for private repos
20
27
 
21
28
  Public GitHub reads work without configuration. To let the librarian search and read private GitHub repositories, provide a token in one of these ways:
@@ -23,13 +30,7 @@ Public GitHub reads work without configuration. To let the librarian search and
23
30
  1. Set `GITHUB_TOKEN` or `GH_TOKEN` in the environment before starting pi.
24
31
  2. Or authenticate the GitHub CLI so `gh auth token` returns a token:
25
32
 
26
- The token is loaded once per pi session and passed to GitHub REST calls used by `search_repos`, `search_github_code`, `checkout_repo`, and `read_github_file`. For private repositories, use a token with read access to the target repos.
27
-
28
- ## Usage
29
-
30
- - Ask pi a question involving other repos; it delegates to the `librarian` tool.
31
- - Ask follow-up questions to earlier librarian runs.
32
- - `/librarian` attaches the research tools directly to your session for manual lookups.
33
+ The token is loaded once per pi session and used for github access in `search_repos`, `search_github_code`, `checkout_repo`, and `read_github_file`. For private repositories, use a token with read access to the target repos.
33
34
 
34
35
  ## Configuration
35
36
 
@@ -40,7 +41,7 @@ In pi's global `settings.json`:
40
41
  "librarian": {
41
42
  "model": "openai-codex/gpt-5.5",
42
43
  "thinkingLevel": "off",
43
- "tools": ["search_web", "fetch_web"],
44
+ "tools": ["web_search", "fetch_content", "get_search_content"],
44
45
  "extensions": ["~/.pi/agent/extensions/parallel-web-tools"],
45
46
  "cacheDir": "/tmp/pi-librarian",
46
47
  "debug": { "persistRuns": false },
@@ -48,16 +49,16 @@ In pi's global `settings.json`:
48
49
  }
49
50
  ```
50
51
 
51
- | Setting | Recommended | Default |
52
- | ------------------- | ----------------------------------------------- | ------------------------------ |
53
- | `model` | `openai-codex/gpt-5.5` | current session model |
54
- | `thinkingLevel` | `off` | current session thinking level |
55
- | `tools` | names of extra tools to activate, when needed | `[]` |
56
- | `extensions` | escape hatch paths for tools not loaded in pi | `[]` |
57
- | `cacheDir` | `/tmp/pi-librarian` | `/tmp/pi-librarian` |
58
- | `debug.persistRuns` | persist nested session file paths for debugging | `false` |
52
+ | Setting | Recommended | Default |
53
+ | ------------------- | ------------------------------------------------ | ------------------------------ |
54
+ | `model` | `openai-codex/gpt-5.5` | current session model |
55
+ | `thinkingLevel` | `off` | current session thinking level |
56
+ | `tools` | names of extra tools to provide the librarian | `[]` |
57
+ | `extensions` | extra paths to load extensions for the librarian | `[]` |
58
+ | `cacheDir` | `/tmp/pi-librarian` | `/tmp/pi-librarian` |
59
+ | `debug.persistRuns` | persist nested session file paths for debugging | `false` |
59
60
 
60
- `librarian.tools` is the activation gate for extra tools. `librarian.extensions` only adds extension paths to the search space when a named tool is not already loaded in the main pi session; listing an extension path does not activate every tool in that bundle. Librarian runs exclude `write` and `edit`.
61
+ `librarian.extensions` dynamically loads extra extensions just for the librarian. Add any tools from that extension to `librarian.tools` for the librarian to actually be able to use them. Librarian excludes `write` and `edit`.
61
62
 
62
63
  ## Development
63
64
 
package/RELEASE.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  # Release notes
4
4
 
5
+ ## 0.3.1
6
+
7
+ ### Changed
8
+
9
+ - Sharpened the research tools' prompt snippets, guidelines, and descriptions so the librarian names each tool explicitly and reads its guidance as directive instructions.
10
+
11
+ ## 0.3.0
12
+
13
+ Hardens librarian research tools and aligns GitHub file reads with pi's native `read` tool.
14
+
15
+ ### Changed
16
+
17
+ - Changed `read_github_file` to use `offset` and `limit` instead of `range`, matching pi's native `read` tool semantics.
18
+ - Hardened `checkout_repo` cache reuse so mismatched or malformed cached paths are discarded, while reused checkouts are reset and cleaned before research.
19
+
5
20
  ## 0.2.1
6
21
 
7
22
  Fixes TUI rendering corruption.
@@ -0,0 +1,136 @@
1
+ # Librarian research-tool hardening
2
+
3
+ ## Status
4
+
5
+ Partially implemented
6
+
7
+ ## Decision Summary
8
+
9
+ Capture the hardening work implemented for librarian runs: disposable checkout-cache mechanics and remote file reads that behave like pi's native `read` tool. AST-aware local-code tools and heavier result metadata remain deferred ideas, not active implementation pressure.
10
+
11
+ ## Problem Statement / Background
12
+
13
+ The current librarian design already makes the main reliability move: within-repo research happens from local blob-less clones, while search APIs are candidate finders. A survey of adjacent implementations suggested several refinements worth preserving before the details are lost:
14
+
15
+ - Mitsuhiko's librarian skill treats checkouts as stable cached infrastructure: repo references normalize to a predictable cache path, clones are reused, and refreshes are throttled.
16
+ - Code-research tools such as Octocode and codebase-research-agent emphasize exact regions, line anchors, and smaller useful code chunks rather than raw search dumps.
17
+ - This project should borrow those patterns only where they simplify the librarian's operating surface. It should not become an API-first search tool, a heavy MCP service, or a semantic index whose snippets are mistaken for evidence.
18
+
19
+ The useful scenario is a repeated question about the same repo: the librarian should reuse the existing clone, force it back to the requested upstream state subject to fetch debounce, clean away any cache-local debris, and let the agent read GitHub files with the same mental model it uses for local files.
20
+
21
+ ## Goals
22
+
23
+ - Reuse cached clones safely while preserving the librarian's read-only research contract.
24
+ - Treat checkout-cache contents as disposable infrastructure, not user-owned worktrees.
25
+ - Make `read_github_file` operationally match pi's native `read` tool where possible.
26
+
27
+ ## Non-Goals
28
+
29
+ - No dependency on Octocode or any external MCP service.
30
+ - No shift away from clone-first within-repo research.
31
+ - No semantic/vector index as part of this design.
32
+ - No AST-aware local-code primitive for now; useful, but not part of this hardening pass.
33
+ - No broad tool-result metadata expansion unless a concrete enforcement feature needs it.
34
+ - No repo-profile cache for now; the value is less clear than the hardening work captured here.
35
+
36
+ ## Exposed Shape
37
+
38
+ These additions would preserve the current user-facing `librarian` tool and `/librarian` attach command. The exposed changes would be inside the repo-tool surface:
39
+
40
+ - `checkout_repo` becomes stricter about cache reuse by resetting and cleaning cache entries deterministically.
41
+ - `read_github_file` uses `offset`/`limit` semantics and output conventions that closely mirror pi's native `read` tool.
42
+ - Existing `provide_results.locations` remains the final citation surface. Search results are still candidates by prompt discipline, not by a new metadata framework.
43
+
44
+ ## Design Decisions
45
+
46
+ ### 1. Cached checkouts are disposable infrastructure, not worktrees
47
+
48
+ A reused checkout should be forced back to the requested upstream state, subject only to the fetch debounce. Unlike a human working clone, librarian checkouts are cache infrastructure. If a previous run, aborted process, or manual edit left local changes, generated files, or an old branch behind, `checkout_repo` should not preserve them. Manual edits inside the checkout cache are undefined behavior.
49
+
50
+ For default-branch checkouts, the desired shape is:
51
+
52
+ 1. verify the cache path is a git repo for the requested normalized remote;
53
+ 2. fetch from origin unless the fetch debounce says the clone is fresh enough;
54
+ 3. checkout the default branch;
55
+ 4. hard reset to `origin/<defaultBranch>`;
56
+ 5. remove untracked files with `git clean -fdx`;
57
+ 6. return the pinned HEAD sha and cache status.
58
+
59
+ For explicit refs, the tool should fetch the requested ref when needed, force checkout/detach to the resolved commit, hard reset to that commit, and clean the tree. If the cached remote does not match the requested repo, or if the cache path is malformed, the safe behavior is to discard the cache path and clone again.
60
+
61
+ **Tradeoff:** this destroys any local modifications inside the checkout cache. That is acceptable because librarian runs are read-only by contract; cache contents are not user-owned work.
62
+
63
+ ### 2. Remote file reads should behave like local reads
64
+
65
+ `read_github_file` is most useful when it feels like pi's native `read` tool pointed at GitHub. The agent should not need one mental model for local checkout files and another for remote GitHub files.
66
+
67
+ The tool should use `offset` and `limit` parameters rather than a bespoke `[startLine, endLine]` range:
68
+
69
+ - `offset` is a 1-indexed starting line;
70
+ - `limit` is the maximum number of lines to return;
71
+ - default behavior should match native `read` as closely as practical;
72
+ - clipped output should tell the agent how to continue with `offset`/`limit`;
73
+ - file output should be directly returned as text, not written to disk;
74
+
75
+ Directory listings may remain GitHub-specific, but file reads should be operationally interchangeable with local `read` output. This is more useful than extra metadata because it changes the agent's actual operating surface.
76
+
77
+ **Tradeoff:** changing from `range` to `offset`/`limit` breaks the current tool schema. That is acceptable if it makes the tool more pi-native and easier for agents to use consistently.
78
+
79
+ ### 3. AST and metadata expansion remain deferred
80
+
81
+ Plain grep plus `read` remains the right baseline because it is universal, fast, and transparent. A future AST-aware addition could still be useful, probably as `read_enclosing_node(path, line, language?)` for TypeScript/JavaScript first, but it should wait until the simpler read and checkout semantics are solid.
82
+
83
+ Likewise, broad result metadata such as `evidenceKind`, `complete`, `truncated`, `continuation`, and `recoveryHint` should not be added speculatively. Agents primarily use tool text and prompt instructions; hidden metadata only matters when a renderer or validator consumes it. If citation honesty needs more structure later, the better first step is validating `provide_results.locations` against the run trace: files checked out, files read, line ranges observed, and search hits that were only candidates.
84
+
85
+ **Tradeoff:** this leaves candidate-vs-proof enforcement mostly in the prompt for now. That is acceptable under KISS until there is a concrete validation feature to support.
86
+
87
+ ## Edge Cases & Failure Modes
88
+
89
+ - **Cached repo remote mismatch:** discard and reclone rather than trying to repair an ambiguous cache entry.
90
+ - **Dirty cached checkout:** hard reset and `git clean -fdx`; do not preserve changes.
91
+ - **Fetch fails but stale clone exists:** report a structured error instead of silently researching stale code, unless a future explicit offline mode exists.
92
+ - **Explicit commit not reachable after fetch:** return a ref-resolution error with a recovery hint to verify the ref or repo.
93
+ - **Large GitHub file:** return a clipped line window using native-read-like semantics and tell the agent the next `offset`/`limit` to request.
94
+ - **Search result clipped by limits:** keep the text honest about limits; do not add metadata unless a concrete consumer needs it.
95
+
96
+ ## Alternatives
97
+
98
+ ### Keep checkout cache reuse as-is
99
+
100
+ - **Status:** Rejected
101
+ - **Decision:** The current implementation already does a force checkout/reset for the default branch and force checkout for explicit refs, but it does not fully encode the disposable-cache policy described here.
102
+ - **Discussion:** The stricter policy is easier to reason about and matches the contract: checkout-cache contents are not user-owned work.
103
+
104
+ ### Add Octocode as a dependency
105
+
106
+ - **Status:** Rejected
107
+ - **Decision**: Octocode is an external code-research/MCP-style system, not a pi-native tool. Its useful contribution here is contract design: exact regions, line anchors, pagination, bulk/error semantics, and candidate-vs-proof discipline.
108
+ - **Discussion:** Borrow only the interaction ideas that make pi-native tools easier to use. Do not import the infrastructure or add metadata for its own sake.
109
+
110
+ ### Add repo profiles
111
+
112
+ - **Status:** Rejected
113
+ - **Decision**: Repo profiles are less compelling for now than cache correctness and native-read-like remote file access.
114
+ - **Discussion:** A profile cache could store package manager, source roots, manifests, and test commands, but it risks becoming stale orientation data that agents over-trust. Evidence should continue to come from actual file reads.
115
+
116
+ ## Implementation Plan
117
+
118
+ - [x] Phase 1: Checkout cache hardening
119
+ - Goal: Make reused clones deterministic disposable infrastructure that match the requested upstream ref, subject to fetch debounce.
120
+ - Files: `extensions/librarian/checkout.ts`, `extensions/librarian/tools/checkout-repo.ts`, checkout tests.
121
+ - Work: Verify cached remote identity; discard/reclone on mismatch or malformed cache; fetch with existing debounce; hard reset and `git clean -fdx` for default branch and explicit refs.
122
+ - Validation: Unit tests for dirty checkout cleanup, untracked file cleanup, remote mismatch reclone, explicit ref checkout, malformed cache replacement, and fetch failure behavior; `npm run check`.
123
+
124
+ - [x] Phase 2: Native-read-like `read_github_file`
125
+ - Goal: Make remote GitHub file reads operationally match pi's native `read` tool.
126
+ - Files: `extensions/librarian/tools/read-github-file.ts`, prompt/tests.
127
+ - Work: Replace `range` with `offset`/`limit`; match native read truncation and continuation language as closely as practical; keep direct text output and directory listing behavior.
128
+ - Validation: Tool tests for default truncation, offset/limit reads, clipped continuation text, invalid ranges, directory listings, and pinned `ref` reads; `npm run check`.
129
+
130
+ - [ ] Deferred: citation validation
131
+ - Goal: If citation honesty needs structural enforcement, validate `provide_results.locations` against the run trace instead of adding broad metadata first.
132
+ - Work: Track which checked-out or GitHub files were actually read, including covered line ranges; reject or warn on locations sourced only from search candidates.
133
+
134
+ - [ ] Deferred: AST-aware local-code primitive
135
+ - Goal: Provide a syntactic-region helper only if grep/read chains prove too wasteful.
136
+ - Work: Consider `read_enclosing_node(path, line, language?)`, likely TypeScript/JavaScript first.
@@ -148,6 +148,17 @@ async function pathExists(target: string): Promise<boolean> {
148
148
  }
149
149
  }
150
150
 
151
+ async function currentRemoteKey(
152
+ dest: string,
153
+ signal: AbortSignal | undefined,
154
+ ): Promise<string | undefined> {
155
+ try {
156
+ return parseRepoReference(await git(["remote", "get-url", "origin"], dest, signal)).key;
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
151
162
  async function clone(
152
163
  reference: RepoReference,
153
164
  dest: string,
@@ -207,7 +218,13 @@ export async function checkoutRepo(
207
218
  const repo = parseRepoReference(repoInput);
208
219
  const dest = repoCachePath(cacheDir, repo.key);
209
220
 
210
- const reusedClone = await pathExists(path.join(dest, ".git"));
221
+ const reusedClone =
222
+ (await pathExists(path.join(dest, ".git"))) &&
223
+ (await currentRemoteKey(dest, signal)) === repo.key;
224
+ if (!reusedClone) {
225
+ await fs.rm(dest, { recursive: true, force: true });
226
+ }
227
+
211
228
  if (reusedClone) {
212
229
  await fetchIfStale(dest, signal);
213
230
  } else {
@@ -223,6 +240,7 @@ export async function checkoutRepo(
223
240
  // Refs already fetched (e.g. a sha reachable from an earlier fetch) check out directly.
224
241
  await git(["checkout", "--force", "--detach", ref], dest, signal);
225
242
  }
243
+ await git(["reset", "--hard", "HEAD"], dest, signal);
226
244
  checkedOutRef = ref;
227
245
  } else {
228
246
  const branch = await defaultBranch(dest, signal);
@@ -231,6 +249,8 @@ export async function checkoutRepo(
231
249
  checkedOutRef = branch;
232
250
  }
233
251
 
252
+ await git(["clean", "-fdx"], dest, signal);
253
+
234
254
  const headSha = await git(["rev-parse", "HEAD"], dest, signal);
235
255
  const files = await git(["ls-files"], dest, signal);
236
256
  const fileCount = files.length === 0 ? 0 : files.split("\n").length;
@@ -28,7 +28,14 @@ function repoFullName(repo: GitHubRepoSlug): string {
28
28
  return `${repo.owner}/${repo.repo}`;
29
29
  }
30
30
 
31
- export type GitHubClientProvider = () => Promise<GitHubClient>;
31
+ export interface GitHubClientApi {
32
+ searchRepositories(params: SearchRepositoriesParams): Promise<RepoSearchResult>;
33
+ searchCode(params: SearchGitHubCodeParams): Promise<GitHubCodeSearchResult>;
34
+ readContents(params: ReadContentsParams): Promise<FileContents | DirectoryContents>;
35
+ getRepo(repo: GitHubRepoSlug): Promise<RepoMeta>;
36
+ }
37
+
38
+ export type GitHubClientProvider = () => Promise<GitHubClientApi>;
32
39
 
33
40
  export async function resolveGitHubToken(): Promise<string | undefined> {
34
41
  const envToken = process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
@@ -249,7 +256,7 @@ function buildGitHubCodeQuery(params: SearchGitHubCodeParams): string {
249
256
  return parts.join(" ");
250
257
  }
251
258
 
252
- export class GitHubClient {
259
+ export class GitHubClient implements GitHubClientApi {
253
260
  constructor(private readonly octokit: Octokit) {}
254
261
 
255
262
  async searchRepositories(params: SearchRepositoriesParams): Promise<RepoSearchResult> {
@@ -29,10 +29,10 @@ export function createCheckoutRepoTool(cacheDir: string) {
29
29
  name: LIBRARIAN_TOOL_NAMES.checkoutRepo,
30
30
  label: "Checkout repo",
31
31
  description: "Clone a repo (blob-less partial clone, cached locally).",
32
- promptSnippet: "Clone a repo",
32
+ promptSnippet: "Clone a repo locally",
33
33
  promptGuidelines: [
34
- "Use when you want to deep dive on a specific repo. Follow up using grep/read/find/ls on the returned path, and `git -C <path> log/blame/diff` for history.",
35
- "Do not checkout repos directly using `git`. Always use this tool instead.",
34
+ "Use checkout_repo to deep dive on a specific repo, then grep/read/find/ls on the returned path and `git -C <path> log/blame/diff` for history.",
35
+ "Do not clone repos directly with `git`; always use checkout_repo.",
36
36
  ],
37
37
  parameters: CheckoutRepoParams,
38
38
 
@@ -46,10 +46,9 @@ export function createProvideResultsTool(onFindings: (findings: Findings) => voi
46
46
  name: LIBRARIAN_TOOL_NAMES.provideResults,
47
47
  label: "Provide results",
48
48
  description: "Report your findings in structured form.",
49
- promptSnippet: "Provide results",
49
+ promptSnippet: "Report findings",
50
50
  promptGuidelines: [
51
- "Tool must be called before the turn ends.",
52
- "After calling this tool, the turn will end.",
51
+ "provide_results ends the turn; call it once, after you've gathered your findings.",
53
52
  ],
54
53
  parameters: FindingsSchema,
55
54
 
@@ -1,10 +1,13 @@
1
- import { defineTool } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ DEFAULT_MAX_BYTES,
3
+ defineTool,
4
+ formatSize,
5
+ truncateHead,
6
+ } from "@earendil-works/pi-coding-agent";
2
7
  import { Type } from "typebox";
3
8
  import { GitHubApiError, type GitHubClientProvider } from "../github.ts";
4
9
  import { LIBRARIAN_TOOL_NAMES } from "./names.ts";
5
10
 
6
- const MAX_LINES_WITHOUT_RANGE = 1200;
7
-
8
11
  export interface ReadGitHubFileDetails {
9
12
  kind: "read_github_file";
10
13
  owner: string;
@@ -17,15 +20,24 @@ export interface ReadGitHubFileDetails {
17
20
  const ReadGitHubFileParams = Type.Object({
18
21
  owner: Type.String({ description: "Repository owner or organization." }),
19
22
  repo: Type.String({ description: "Repository name." }),
20
- path: Type.String({ description: "File or directory path within the repository." }),
23
+ path: Type.String({
24
+ description: "File or directory path within the repository.",
25
+ }),
21
26
  ref: Type.Optional(
22
- Type.String({ description: "Branch, tag, or commit sha. Default: the default branch." }),
27
+ Type.String({
28
+ description: "Branch, tag, or commit sha. Default: the default branch.",
29
+ }),
30
+ ),
31
+ offset: Type.Optional(
32
+ Type.Number({
33
+ minimum: 1,
34
+ description: "Line number to start reading from (1-indexed).",
35
+ }),
23
36
  ),
24
- range: Type.Optional(
25
- Type.Array(Type.Number({ minimum: 1 }), {
26
- description: "[startLine, endLine] (1-based, inclusive) for large files.",
27
- minItems: 2,
28
- maxItems: 2,
37
+ limit: Type.Optional(
38
+ Type.Number({
39
+ minimum: 1,
40
+ description: "Maximum number of lines to read.",
29
41
  }),
30
42
  ),
31
43
  });
@@ -34,19 +46,22 @@ export function createReadGitHubFileTool(githubClient: GitHubClientProvider) {
34
46
  return defineTool<typeof ReadGitHubFileParams, ReadGitHubFileDetails>({
35
47
  name: LIBRARIAN_TOOL_NAMES.readGitHubFile,
36
48
  label: "Read GitHub file",
37
- description:
38
- "Read a single file (or list a directory) from a GitHub repo via the API, without cloning.",
39
- promptSnippet: "Read Github file",
49
+ description: `Read a single file (or list a directory) from a GitHub repo via the API, without cloning.`,
50
+ promptSnippet: "Read a file/directory from GitHub",
40
51
  promptGuidelines: [
41
- "For quick peeks — package.json, a README, one source file.",
42
- "For multi-file exploration prefer checkout_repo.",
52
+ "Use read_github_file for quick peeks — package.json, a README, one source file.",
53
+ "For multi-file exploration, prefer checkout_repo over read_github_file.",
54
+ "Use read_github_file's offset/limit for larger files.",
43
55
  ],
44
56
  parameters: ReadGitHubFileParams,
45
57
 
46
58
  async execute(_toolCallId, params) {
47
- const range = params.range ? (params.range as [number, number]) : undefined;
48
- if (range && range[1] < range[0]) {
49
- throw new Error("range end must be greater than or equal to range start.");
59
+ const offset = params.offset ?? 1;
60
+ if (offset < 1) {
61
+ throw new Error("offset must be greater than or equal to 1.");
62
+ }
63
+ if (params.limit !== undefined && params.limit < 1) {
64
+ throw new Error("limit must be greater than or equal to 1.");
50
65
  }
51
66
 
52
67
  try {
@@ -75,32 +90,52 @@ export function createReadGitHubFileTool(githubClient: GitHubClientProvider) {
75
90
  }
76
91
 
77
92
  const allLines = contents.text.split("\n");
78
- let start = 1;
79
- let end = allLines.length;
80
- let clipNote = "";
81
-
82
- if (range) {
83
- start = Math.min(range[0], allLines.length);
84
- end = Math.min(range[1], allLines.length);
85
- } else if (allLines.length > MAX_LINES_WITHOUT_RANGE) {
86
- end = MAX_LINES_WITHOUT_RANGE;
87
- clipNote = `\n... clipped at line ${MAX_LINES_WITHOUT_RANGE} of ${allLines.length}; pass range to read further.`;
93
+ const startLine = offset - 1;
94
+ if (startLine >= allLines.length) {
95
+ throw new Error(
96
+ `Offset ${offset} is beyond end of file (${allLines.length} lines total)`,
97
+ );
88
98
  }
89
99
 
90
- const width = String(end).length;
91
- const numbered = allLines
92
- .slice(start - 1, end)
93
- .map((line, index) => `${String(start + index).padStart(width)}\t${line}`)
94
- .join("\n");
100
+ const selectedLines =
101
+ params.limit === undefined
102
+ ? allLines.slice(startLine)
103
+ : allLines.slice(startLine, Math.min(startLine + params.limit, allLines.length));
104
+ const selectedContent = selectedLines.join("\n");
105
+ const truncated = truncateHead(selectedContent);
106
+ let text: string;
107
+ let lineCount = truncated.outputLines;
108
+
109
+ if (truncated.firstLineExceedsLimit) {
110
+ const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine] ?? "", "utf-8"));
111
+ text = `[Line ${offset} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit.]`;
112
+ } else if (truncated.truncated) {
113
+ const endLine = offset + truncated.outputLines - 1;
114
+ const nextOffset = endLine + 1;
115
+ const limitNote =
116
+ truncated.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;
117
+ text = `${truncated.content}\n\n[Showing lines ${offset}-${endLine} of ${allLines.length}${limitNote}. Use offset=${nextOffset} to continue.]`;
118
+ } else if (
119
+ params.limit !== undefined &&
120
+ startLine + selectedLines.length < allLines.length
121
+ ) {
122
+ const remaining = allLines.length - (startLine + selectedLines.length);
123
+ const nextOffset = startLine + selectedLines.length + 1;
124
+ text = `${truncated.content}\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
125
+ lineCount = selectedLines.length;
126
+ } else {
127
+ text = truncated.content;
128
+ lineCount = selectedLines.length;
129
+ }
95
130
 
96
131
  return {
97
- content: [{ type: "text", text: `${numbered}${clipNote}` }],
132
+ content: [{ type: "text", text }],
98
133
  details: {
99
134
  kind: "read_github_file",
100
135
  owner: params.owner,
101
136
  repo: params.repo,
102
137
  path: params.path,
103
- lineCount: end - start + 1,
138
+ lineCount,
104
139
  isDirectory: false,
105
140
  },
106
141
  };
@@ -18,7 +18,8 @@ const SearchCodeParams = Type.Object({
18
18
  }),
19
19
  regex: Type.Optional(
20
20
  Type.Boolean({
21
- description: "Treat `pattern` as a regular expression instead of literals.",
21
+ description:
22
+ "Treat `pattern` as a regular expression instead of literals. Supports `(?s)` for multi-line patterns.",
22
23
  }),
23
24
  ),
24
25
  repo: Type.Optional(
@@ -59,11 +60,10 @@ export const searchCodeTool = defineTool<typeof SearchCodeParams, SearchCodeDeta
59
60
  name: LIBRARIAN_TOOL_NAMES.searchCode,
60
61
  label: "Search code across repos",
61
62
  description: "Cross-repo code search over public source code.",
62
- promptSnippet: "Search code across repos",
63
+ promptSnippet: "Search public code across repos",
63
64
  promptGuidelines: [
64
- "When `regex` is true, `pattern` can also use `(?s)` for multi-line patterns.",
65
- "Results are CANDIDATES to verify via checkout_repo/read_github_file never cite them directly.",
66
- "This searches public GitHub code only. Use search_github_code when private repository access matters.",
65
+ "search_code results are candidates to verify via checkout_repo/read_github_file never cite them directly.",
66
+ "search_code covers public GitHub code only; use search_github_code when private repository access is required.",
67
67
  ],
68
68
  parameters: SearchCodeParams,
69
69
 
@@ -89,10 +89,9 @@ export function createSearchGitHubCodeTool(githubClient: GitHubClientProvider) {
89
89
  label: "Search GitHub code",
90
90
  description:
91
91
  "GitHub REST code search over public code and private repositories your configured GitHub auth can access.",
92
- promptSnippet: "Search GitHub code",
92
+ promptSnippet: "Search GitHub code (incl. private repos)",
93
93
  promptGuidelines: [
94
- "Results are CANDIDATES to verify via checkout_repo/read_github_file never cite them directly.",
95
- "GitHub REST code search is literal/tokenized and does not support regex; use search_code for public regex/global code search.",
94
+ "search_github_code is literal/tokenized and does not support regex; use search_code for public regex/global search.",
96
95
  ],
97
96
  parameters: SearchGitHubCodeParams,
98
97
 
@@ -34,8 +34,8 @@ export function createSearchReposTool(githubClient: GitHubClientProvider) {
34
34
  name: LIBRARIAN_TOOL_NAMES.searchRepos,
35
35
  label: "Search repos metadata",
36
36
  description: "Discover GitHub repositories.",
37
- promptSnippet: "Search repos metadata",
38
- promptGuidelines: ["Use for questions like 'what are the popular X libraries'."],
37
+ promptSnippet: "Search GitHub repos by metadata",
38
+ promptGuidelines: ['Use search_repos for questions like "what are the popular X libraries".'],
39
39
  parameters: SearchReposParams,
40
40
 
41
41
  async execute(_toolCallId, params) {
@@ -61,7 +61,6 @@ export default function librarianExtension(pi: ExtensionAPI): void {
61
61
  for (const tool of attachableTools) {
62
62
  pi.registerTool({
63
63
  ...tool,
64
- description: `${tool.description} (Librarian tool, attached via /librarian: use for quick single lookups; delegate multi-step research to the librarian tool.)`,
65
64
  renderCall(args, theme) {
66
65
  const { verb, subject } = formatTraceLine(
67
66
  { name: tool.name, args, id: "", startedAt: 0 } satisfies TraceCall,
@@ -81,10 +80,9 @@ export default function librarianExtension(pi: ExtensionAPI): void {
81
80
  label: "Librarian",
82
81
  description:
83
82
  "Understand complex, multi-repo codebases, exploring cross-repo relationships, analyzing architectural patterns, finding implementations across codebases, understanding code evolution/commit history, diving deep on specific code bases, getting comprehensive feature explanations, and exploring end-to-end system design across within or across repositories.",
84
- promptSnippet:
85
- "librarian: research GitHub repos (implementation questions, ecosystem comparisons, usage examples) and return cited findings",
83
+ promptSnippet: "Research GitHub repos and codebases, returning cited findings",
86
84
  promptGuidelines: [
87
- "If a single file or reference must be located, you may use other means (search for downloaded dependencies, web search, etc), but for deeper queries that will take multiple steps to resolve, prefer the librarian.",
85
+ "Use librarian for deep, multi-step research across repos; for a single file or reference, prefer cheaper means (downloaded dependencies, web search or fetch).",
88
86
  ],
89
87
  parameters: LibrarianParams,
90
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thurstonsand/pi-librarian",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "GitHub research subagent for pi: deep-dive specific repos, discover across the ecosystem",
6
6
  "license": "MIT",