@thurstonsand/pi-librarian 0.3.0 → 0.4.0

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,25 @@
2
2
 
3
3
  # Release notes
4
4
 
5
+ ## 0.4.0
6
+
7
+ ### Added
8
+
9
+ - Added compact, expandable transcript entries for `/librarian` attach and detach changes, including the exact repo tools affected.
10
+ - Added support for Pi's `max` thinking level.
11
+
12
+ ### Changed
13
+
14
+ - Changed `librarian.model` to use Pi's native model-pattern resolution.
15
+ - Raised the minimum supported Pi version to 0.80.6.
16
+ - Changed persisted attach entries to require tool snapshots. Older attach entries are now ignored.
17
+
18
+ ## 0.3.1
19
+
20
+ ### Changed
21
+
22
+ - Sharpened the research tools' prompt snippets, guidelines, and descriptions so the librarian names each tool explicitly and reads its guidance as directive instructions.
23
+
5
24
  ## 0.3.0
6
25
 
7
26
  Hardens librarian research tools and aligns GitHub file reads with pi's native `read` tool.
@@ -0,0 +1,171 @@
1
+ # Pi 0.80.6 attach entries, thinking, and model resolution
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Decision Summary
8
+
9
+ Adopt Pi 0.80.6's custom-entry renderer, CLI model resolver, and `max` thinking level. Attach state changes become durable transcript accordions with historical tool snapshots; configured models follow Pi's native model-pattern semantics and fall back to the current session model only when Pi cannot resolve them.
10
+
11
+ ## Problem Statement / Background
12
+
13
+ Pi-librarian already persists `/librarian` attach state as `pi-librarian:attach` custom entries, but those entries are invisible in the transcript. The command compensates with transient notifications, so a resumed session can restore its state without showing when or how the state changed.
14
+
15
+ Pi 0.80.4 added `registerEntryRenderer`, which can present state records without sending them to the model. The rendered entry must snapshot the affected tool names so historical entries remain accurate if pi-librarian's attachable toolset changes later; older incomplete shapes are deliberately unsupported.
16
+
17
+ Configured librarian models currently use local fuzzy matching over `modelRegistry.getAvailable()`. Pi exports `resolveCliModel`, the canonical resolver for exact references, bare and provider-scoped fuzzy patterns, and custom model IDs. Pi-librarian should use that behavior directly rather than impose a separate availability policy.
18
+
19
+ Pi 0.80.6 extends the public `ThinkingLevel` type with `max`. Pi exposes model-specific `getSupportedThinkingLevels`, but no public runtime list of every accepted setting value, so settings validation must keep one local runtime list aligned with Pi's public type.
20
+
21
+ ## Goals
22
+
23
+ - Make actual attach and detach changes visible as durable, compact transcript history without duplicate notifications.
24
+ - Preserve the exact toolset affected by each new historical entry.
25
+ - Reject attach entries that do not match the current data shape without migration behavior.
26
+ - Use Pi's canonical configured-model resolution and fallback to the current session model only on resolution failure.
27
+ - Make every configured-model fallback visible and actionable.
28
+ - Accept and preserve Pi 0.80.6's `max` thinking level from settings through nested runs.
29
+
30
+ ## Non-Goals
31
+
32
+ - Persist `/librarian status` queries or repeated explicit `on`/`off` no-ops.
33
+ - Send attach state entries to the LLM.
34
+ - Change nested-run extension loading to use `InlineExtension`.
35
+ - Add a separate authentication or availability gate after Pi resolves a configured model.
36
+ - Choose a different fallback model when no current session model exists.
37
+
38
+ ## Exposed Shape
39
+
40
+ ### `/librarian` command feedback
41
+
42
+ An actual state change appends one `pi-librarian:attach` custom entry and does not also show a notification. `/librarian status` remains a transient notification. `/librarian on` while attached and `/librarian off` while detached remain transient no-ops, using symmetrical wording:
43
+
44
+ - `Librarian tools already attached.`
45
+ - `Librarian tools already detached.`
46
+
47
+ ### Attach transcript entry
48
+
49
+ The compact line is stable when expanded:
50
+
51
+ - `Librarian tools attached`
52
+ - `Librarian tools detached`
53
+
54
+ Attached state uses success styling; detached state uses muted styling. Expanding a current entry reveals the exact affected tool names beneath the unchanged compact line. Tool names appear in their registered order.
55
+
56
+ New entries persist:
57
+
58
+ ```ts
59
+ {
60
+ attached: boolean;
61
+ tools: string[];
62
+ }
63
+ ```
64
+
65
+ Both attach and detach entries snapshot the complete affected tool list. Any entry that does not match the complete current shape renders `Librarian attach state unavailable` and does not influence restored state.
66
+
67
+ ### Thinking-level settings
68
+
69
+ `librarian.thinkingLevel` accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. The selected value crosses settings, model resolution, and nested-run boundaries unchanged; provider/model capability handling remains Pi's responsibility. Because Pi has no public runtime constant for the complete setting vocabulary, pi-librarian keeps one local list checked against Pi's public `ThinkingLevel` type.
70
+
71
+ ### Configured model resolution
72
+
73
+ `librarian.model` accepts the same patterns as Pi: exact or fuzzy bare patterns such as `opus`, provider-scoped patterns such as `anthropic/opus`, and custom model IDs supported by `resolveCliModel`. `resolveLibrarianModel` passes the pattern directly to Pi against `modelRegistry.getAll()` and accepts any returned model. Resolver warnings, including custom-ID fallback warnings, are shown without rejecting the model.
74
+
75
+ When configured resolution returns an error or no model, each librarian invocation:
76
+
77
+ 1. falls back to `ctx.model` when present;
78
+ 2. emits a warning naming the configured model, the failure reason, and the current fallback model; and
79
+ 3. runs with the requested librarian thinking level unchanged.
80
+
81
+ If no current model exists, the librarian tool retains its existing execution error. No warning is needed in the successful configured-model or unconfigured current-model paths.
82
+
83
+ ## Design Decisions
84
+
85
+ ### 1. Entries record state changes, not every command response
86
+
87
+ Only actual state changes belong in durable transcript history. Status queries and repeated explicit states do not change session state, so notifications remain the appropriate surface. Removing the transition notification avoids presenting the same event twice.
88
+
89
+ ### 2. New entries snapshot the affected tools
90
+
91
+ Deriving names from the current `ATTACHABLE_TOOL_NAMES` would make old transcript entries change meaning after the toolset evolves. Storing the names adds minor duplication but preserves historical truth. Detach entries retain the list because removal has the same affected scope as attachment.
92
+
93
+ ### 3. Entry rendering has no compatibility shape
94
+
95
+ Attach entries either match the complete current `{ attached, tools }` shape or render the generic unavailable-state line. Carrying a legacy branch would preserve data that cannot satisfy the historical-toolset requirement.
96
+
97
+ ### 4. Expansion is additive
98
+
99
+ Expanded rendering keeps the compact line unchanged and adds details below it. This treats transcript expansion as an accordion rather than substituting one representation for another.
100
+
101
+ ### 5. Pi owns model resolution
102
+
103
+ `resolveCliModel` owns provider normalization, fuzzy matching, priority, and custom model-ID fallback. Pi-librarian accepts its result directly. Authentication and provider failures belong to nested execution rather than a second model-selection policy.
104
+
105
+ ### 6. Fallback warnings occur on every affected invocation
106
+
107
+ Repeated warnings are intentional. Every run that differs from configured intent should say so at the point of use; silently deduplicating could hide a continuing configuration problem from later research runs.
108
+
109
+ ## Edge Cases & Failure Modes
110
+
111
+ - **Old or malformed attach entry:** does not alter restored state and renders a neutral unavailable-state line.
112
+ - **Toolset changes after an entry was written:** new rendering uses the entry's snapshot, preserving historical scope.
113
+ - **Repeated explicit state:** appends no entry and reports that tools are already attached or detached.
114
+ - **Unknown provider:** warns with Pi's resolution reason and falls back to the current model.
115
+ - **Resolved but unauthenticated model:** is selected; nested execution reports any resulting authentication failure.
116
+ - **Resolver-created custom model ID:** is selected and Pi's warning is shown.
117
+ - **No configured model:** uses the current model without warning.
118
+ - **No usable configured or current model:** librarian execution throws the existing no-model error.
119
+
120
+ ## Alternatives
121
+
122
+ ### Keep transient transition notifications alongside entries
123
+
124
+ - **Status:** Rejected
125
+ - **Decision:** It duplicates one event in two UI surfaces. The durable entry is sufficient feedback for an actual state change.
126
+
127
+ ### Derive tool names while rendering
128
+
129
+ - **Status:** Rejected
130
+ - **Decision:** Historical entries would silently change when the attachable toolset changes.
131
+
132
+ ### Version the entry immediately
133
+
134
+ - **Status:** Rejected
135
+ - **Decision:** There is one accepted shape and no migration behavior. A version adds ceremony without distinguishing supported semantics.
136
+
137
+ ### Filter resolved models through `getAvailable()`
138
+
139
+ - **Status:** Rejected
140
+ - **Decision:** It diverges from Pi's native model-pattern behavior and blocks intentional custom or differently authenticated models. Nested execution owns operational model failures.
141
+
142
+ ### Warn once at startup or once per session
143
+
144
+ - **Status:** Rejected
145
+ - **Decision:** Each resolver warning or fallback should remain explicit at its point of use.
146
+
147
+ ## Implementation Plan
148
+
149
+ - [x] Phase 1: Raise the Pi dependency floor
150
+ - Goal: Make the new Pi APIs and thinking level part of pi-librarian's supported runtime contract.
151
+ - Files: `package.json`, `package-lock.json`, `extensions/librarian/settings.ts`, tests.
152
+ - Work: Raise relevant `@earendil-works/pi-*` development and peer dependency minimums to 0.80.6, refresh the npm lockfile, and accept `max` through a local runtime list checked against Pi's public `ThinkingLevel` type.
153
+ - Validation: Inspect root constraints and resolved package versions; include package validation in the full quality gate.
154
+
155
+ - [x] Phase 2: Render durable attach state
156
+ - Goal: Replace duplicate transition notifications with strict-shape transcript accordions.
157
+ - Files: `extensions/librarian/attach.ts`, `extensions/librarian.ts`, `test/attach.test.ts`.
158
+ - Work: Define the current entry shape; snapshot tool names from `ATTACHABLE_TOOL_NAMES`; register the entry renderer; preserve compact content while expanded; render every non-current shape generically; remove actual-transition notifications; make no-op wording symmetrical.
159
+ - Validation: Focused tests for state restoration, persisted snapshots, compact and expanded rendering, generic invalid-shape rendering, and active-tool mutation.
160
+
161
+ - [x] Phase 3: Adopt canonical model resolution
162
+ - Goal: Use `resolveCliModel` with Pi-native model-pattern and fallback behavior.
163
+ - Files: `extensions/librarian/model.ts`, `extensions/librarian.ts`, `test/model.test.ts`.
164
+ - Work: Resolve bare and provider-scoped configured patterns through Pi; accept returned models and warnings directly; return a specific warning when falling back to the current model; preserve no-model behavior.
165
+ - Validation: Focused tests for bare and provider-scoped fuzzy resolution, Pi model priority, custom-ID fallback, resolution errors, warning text, current fallback, missing fallback, and preservation of `max`.
166
+
167
+ - [x] Phase 4: Integration validation
168
+ - Goal: Prove the upgrade works as one coherent change.
169
+ - Files: implementation and test files above; design status only if implementation materially diverges.
170
+ - Work: Run focused tests, typecheck against the upgraded Pi API, and inspect the final staged/unstaged split without altering it.
171
+ - Validation: `npm test -- test/attach.test.ts test/model.test.ts test/settings.test.ts`; `npm run check`.
@@ -1,21 +1,28 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, Text } from "@earendil-works/pi-tui";
2
3
  import { Type } from "typebox";
3
4
  import { safeParseTypeBoxValue } from "../shared/typebox.ts";
4
5
  import { ATTACHABLE_TOOL_NAMES } from "./tools/names.ts";
5
6
 
6
7
  export const ATTACH_ENTRY_TYPE = "pi-librarian:attach";
7
8
 
8
- const ATTACH_STATE_SCHEMA = Type.Object({
9
+ const ATTACH_ENTRY_SCHEMA = Type.Object({
9
10
  attached: Type.Boolean(),
11
+ tools: Type.Array(Type.String()),
10
12
  });
11
13
 
14
+ export interface AttachEntryData {
15
+ attached: boolean;
16
+ tools: string[];
17
+ }
18
+
12
19
  export function readAttachState(ctx: ExtensionContext): boolean {
13
20
  let attached = false;
14
21
  for (const entry of ctx.sessionManager.getEntries()) {
15
22
  if (entry.type !== "custom" || entry.customType !== ATTACH_ENTRY_TYPE) {
16
23
  continue;
17
24
  }
18
- const parsed = safeParseTypeBoxValue(ATTACH_STATE_SCHEMA, entry.data);
25
+ const parsed = safeParseTypeBoxValue(ATTACH_ENTRY_SCHEMA, entry.data);
19
26
  if (parsed) {
20
27
  attached = parsed.attached;
21
28
  }
@@ -37,5 +44,25 @@ export function applyAttachState(pi: ExtensionAPI, attached: boolean): void {
37
44
 
38
45
  export function setAttachState(pi: ExtensionAPI, attached: boolean): void {
39
46
  applyAttachState(pi, attached);
40
- pi.appendEntry(ATTACH_ENTRY_TYPE, { attached });
47
+ pi.appendEntry<AttachEntryData>(ATTACH_ENTRY_TYPE, {
48
+ attached,
49
+ tools: [...ATTACHABLE_TOOL_NAMES],
50
+ });
51
+ }
52
+
53
+ export function renderAttachEntry(data: unknown, expanded: boolean, theme: Theme): Component {
54
+ const entry = safeParseTypeBoxValue(ATTACH_ENTRY_SCHEMA, data);
55
+ if (!entry) {
56
+ return new Text(theme.fg("warning", "Librarian attach state unavailable"), 0, 0);
57
+ }
58
+
59
+ const state = entry.attached ? "attached" : "detached";
60
+ const color = entry.attached ? "success" : "muted";
61
+ let text = theme.fg(color, `Librarian tools ${state}`);
62
+
63
+ if (expanded) {
64
+ text += `\n${entry.tools.map((tool) => theme.fg("dim", ` ${tool}`)).join("\n")}`;
65
+ }
66
+
67
+ return new Text(text, 0, 0);
41
68
  }
@@ -1,6 +1,6 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Api, Model } from "@earendil-works/pi-ai";
3
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { type ExtensionContext, resolveCliModel } from "@earendil-works/pi-coding-agent";
4
4
  import type { ModelReference } from "./settings.ts";
5
5
 
6
6
  export type LibrarianModelSource = "configured" | "current";
@@ -9,31 +9,21 @@ export interface LibrarianModelResolution {
9
9
  model: Model<Api>;
10
10
  thinkingLevel: ThinkingLevel;
11
11
  source: LibrarianModelSource;
12
+ warning?: string;
12
13
  }
13
14
 
14
- function isAlias(modelId: string): boolean {
15
- return modelId.endsWith("-latest") || !/-\d{8}$/.test(modelId);
16
- }
17
-
18
- function bestModelMatch(models: Model<Api>[], pattern: string): Model<Api> | undefined {
19
- const normalizedPattern = pattern.toLowerCase();
20
- const exactMatches = models.filter((model) => model.id.toLowerCase() === normalizedPattern);
21
- if (exactMatches.length === 1) {
22
- return exactMatches[0];
15
+ function configuredModelFailure(
16
+ configuredModel: ModelReference,
17
+ error: string | undefined,
18
+ warning: string | undefined,
19
+ ): string {
20
+ if (error) {
21
+ return error;
23
22
  }
24
-
25
- const partialMatches = models.filter(
26
- (model) =>
27
- model.id.toLowerCase().includes(normalizedPattern) ||
28
- model.name?.toLowerCase().includes(normalizedPattern),
29
- );
30
- if (partialMatches.length === 0) {
31
- return undefined;
23
+ if (warning) {
24
+ return warning;
32
25
  }
33
-
34
- const aliases = partialMatches.filter((model) => isAlias(model.id));
35
- const candidates = aliases.length > 0 ? aliases : partialMatches;
36
- return candidates.toSorted((a, b) => b.id.localeCompare(a.id))[0];
26
+ return `Could not resolve configured model "${configuredModel}".`;
37
27
  }
38
28
 
39
29
  export function resolveLibrarianModel(
@@ -41,19 +31,41 @@ export function resolveLibrarianModel(
41
31
  configuredModel: ModelReference | undefined,
42
32
  thinkingLevel: ThinkingLevel,
43
33
  ): LibrarianModelResolution | undefined {
34
+ let failure: string | undefined;
35
+
44
36
  if (configuredModel) {
45
- const providerModels = ctx.modelRegistry
46
- .getAvailable()
47
- .filter((model) => model.provider === configuredModel.provider);
48
- const match = bestModelMatch(providerModels, configuredModel.modelId);
49
- if (match) {
50
- return { model: match, thinkingLevel, source: "configured" };
37
+ const resolved = resolveCliModel({
38
+ ...(configuredModel.provider ? { cliProvider: configuredModel.provider } : {}),
39
+ cliModel: configuredModel.modelId,
40
+ modelRegistry: ctx.modelRegistry,
41
+ });
42
+ if (resolved.model) {
43
+ const resolution: LibrarianModelResolution = {
44
+ model: resolved.model,
45
+ thinkingLevel,
46
+ source: "configured",
47
+ };
48
+ if (resolved.warning) {
49
+ resolution.warning = resolved.warning;
50
+ }
51
+ return resolution;
51
52
  }
53
+
54
+ failure = configuredModelFailure(configuredModel, resolved.error, resolved.warning);
52
55
  }
53
56
 
54
57
  if (!ctx.model) {
55
58
  return undefined;
56
59
  }
57
60
 
58
- return { model: ctx.model, thinkingLevel, source: "current" };
61
+ if (!configuredModel) {
62
+ return { model: ctx.model, thinkingLevel, source: "current" };
63
+ }
64
+
65
+ return {
66
+ model: ctx.model,
67
+ thinkingLevel,
68
+ source: "current",
69
+ warning: `Configured librarian model "${configuredModel}" is unavailable: ${failure} Using current model "${ctx.model.provider}/${ctx.model.id}".`,
70
+ };
59
71
  }
@@ -5,13 +5,24 @@ import { SettingsManager } from "@earendil-works/pi-coding-agent";
5
5
  import { type Static, Type } from "typebox";
6
6
  import { parseTypeBoxValue } from "../shared/typebox.ts";
7
7
 
8
+ const THINKING_LEVELS = [
9
+ "off",
10
+ "minimal",
11
+ "low",
12
+ "medium",
13
+ "high",
14
+ "xhigh",
15
+ "max",
16
+ ] as const satisfies readonly ThinkingLevel[];
17
+
8
18
  const THINKING_LEVEL_SCHEMA = Type.Union([
9
- Type.Literal("off"),
10
- Type.Literal("minimal"),
11
- Type.Literal("low"),
12
- Type.Literal("medium"),
13
- Type.Literal("high"),
14
- Type.Literal("xhigh"),
19
+ Type.Literal(THINKING_LEVELS[0]),
20
+ Type.Literal(THINKING_LEVELS[1]),
21
+ Type.Literal(THINKING_LEVELS[2]),
22
+ Type.Literal(THINKING_LEVELS[3]),
23
+ Type.Literal(THINKING_LEVELS[4]),
24
+ Type.Literal(THINKING_LEVELS[5]),
25
+ Type.Literal(THINKING_LEVELS[6]),
15
26
  ]);
16
27
 
17
28
  const LIBRARIAN_FILE_SETTINGS_SCHEMA = Type.Object({
@@ -35,12 +46,12 @@ type LibrarianFileSettings = Static<typeof LIBRARIAN_FILE_SETTINGS_SCHEMA>;
35
46
 
36
47
  export class ModelReference {
37
48
  constructor(
38
- readonly provider: string,
49
+ readonly provider: string | undefined,
39
50
  readonly modelId: string,
40
51
  ) {}
41
52
 
42
53
  toString(): string {
43
- return `${this.provider}/${this.modelId}`;
54
+ return this.provider ? `${this.provider}/${this.modelId}` : this.modelId;
44
55
  }
45
56
  }
46
57
 
@@ -92,7 +103,10 @@ function parseModelReference(value: string | undefined): ModelReference | undefi
92
103
  }
93
104
 
94
105
  const slashIndex = trimmed.indexOf("/");
95
- if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
106
+ if (slashIndex === -1) {
107
+ return new ModelReference(undefined, trimmed);
108
+ }
109
+ if (slashIndex === 0 || slashIndex === trimmed.length - 1) {
96
110
  return undefined;
97
111
  }
98
112
 
@@ -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
 
@@ -47,11 +47,11 @@ export function createReadGitHubFileTool(githubClient: GitHubClientProvider) {
47
47
  name: LIBRARIAN_TOOL_NAMES.readGitHubFile,
48
48
  label: "Read GitHub file",
49
49
  description: `Read a single file (or list a directory) from a GitHub repo via the API, without cloning.`,
50
- promptSnippet: "Read Github file",
50
+ promptSnippet: "Read a file/directory from GitHub",
51
51
  promptGuidelines: [
52
- "For quick peeks — package.json, a README, one source file.",
53
- "For multi-file exploration prefer checkout_repo.",
54
- "Use offset/limit for larger files.",
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.",
55
55
  ],
56
56
  parameters: ReadGitHubFileParams,
57
57
 
@@ -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) {
@@ -1,7 +1,14 @@
1
1
  import type { ExtensionAPI, SessionStartEvent } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
- import { applyAttachState, readAttachState, setAttachState } from "./librarian/attach.ts";
4
+ import {
5
+ ATTACH_ENTRY_TYPE,
6
+ type AttachEntryData,
7
+ applyAttachState,
8
+ readAttachState,
9
+ renderAttachEntry,
10
+ setAttachState,
11
+ } from "./librarian/attach.ts";
5
12
  import { collectExtraToolWarnings, resolveExtraTools } from "./librarian/extra-tools.ts";
6
13
  import { createGitHubClientProvider } from "./librarian/github.ts";
7
14
  import { resolveLibrarianModel } from "./librarian/model.ts";
@@ -50,6 +57,10 @@ export default function librarianExtension(pi: ExtensionAPI): void {
50
57
  const settings = loadSettings();
51
58
  const githubClient = createGitHubClientProvider();
52
59
 
60
+ pi.registerEntryRenderer<AttachEntryData>(ATTACH_ENTRY_TYPE, (entry, { expanded }, theme) =>
61
+ renderAttachEntry(entry.data, expanded, theme),
62
+ );
63
+
53
64
  const attachableTools = [
54
65
  createSearchReposTool(githubClient),
55
66
  searchCodeTool,
@@ -61,7 +72,6 @@ export default function librarianExtension(pi: ExtensionAPI): void {
61
72
  for (const tool of attachableTools) {
62
73
  pi.registerTool({
63
74
  ...tool,
64
- description: `${tool.description} (Librarian tool, attached via /librarian: use for quick single lookups; delegate multi-step research to the librarian tool.)`,
65
75
  renderCall(args, theme) {
66
76
  const { verb, subject } = formatTraceLine(
67
77
  { name: tool.name, args, id: "", startedAt: 0 } satisfies TraceCall,
@@ -81,10 +91,9 @@ export default function librarianExtension(pi: ExtensionAPI): void {
81
91
  label: "Librarian",
82
92
  description:
83
93
  "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",
94
+ promptSnippet: "Research GitHub repos and codebases, returning cited findings",
86
95
  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.",
96
+ "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
97
  ],
89
98
  parameters: LibrarianParams,
90
99
 
@@ -96,6 +105,9 @@ export default function librarianExtension(pi: ExtensionAPI): void {
96
105
  "No model available for the librarian. Configure librarian.model or select a session model.",
97
106
  );
98
107
  }
108
+ if (resolution.warning) {
109
+ ctx.ui.notify(resolution.warning, "warning");
110
+ }
99
111
 
100
112
  const extraTools = resolveExtraTools(pi.getAllTools(), settings);
101
113
 
@@ -169,19 +181,13 @@ export default function librarianExtension(pi: ExtensionAPI): void {
169
181
  argument === "on" ? true : argument === "off" ? false : !currentlyAttached;
170
182
  if (nextAttached === currentlyAttached) {
171
183
  ctx.ui.notify(
172
- nextAttached ? "Librarian tools already attached." : "Librarian tools not attached.",
184
+ nextAttached ? "Librarian tools already attached." : "Librarian tools already detached.",
173
185
  "info",
174
186
  );
175
187
  return;
176
188
  }
177
189
 
178
190
  setAttachState(pi, nextAttached);
179
- ctx.ui.notify(
180
- nextAttached
181
- ? `Attached librarian tools: ${ATTACHABLE_TOOL_NAMES.join(", ")}`
182
- : "Detached librarian tools.",
183
- "info",
184
- );
185
191
  },
186
192
  });
187
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thurstonsand/pi-librarian",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "GitHub research subagent for pi: deep-dive specific repos, discover across the ecosystem",
6
6
  "license": "MIT",
@@ -43,10 +43,10 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.4.14",
46
- "@earendil-works/pi-agent-core": "^0.80.2",
47
- "@earendil-works/pi-ai": "^0.80.2",
48
- "@earendil-works/pi-coding-agent": "^0.80.2",
49
- "@earendil-works/pi-tui": "^0.80.2",
46
+ "@earendil-works/pi-agent-core": "^0.80.6",
47
+ "@earendil-works/pi-ai": "^0.80.6",
48
+ "@earendil-works/pi-coding-agent": "^0.80.6",
49
+ "@earendil-works/pi-tui": "^0.80.6",
50
50
  "@types/node": "^25.6.2",
51
51
  "husky": "^9.1.7",
52
52
  "lint-staged": "^17.0.3",
@@ -55,10 +55,10 @@
55
55
  "vitest": "^4.1.5"
56
56
  },
57
57
  "peerDependencies": {
58
- "@earendil-works/pi-agent-core": ">=0.80.2",
59
- "@earendil-works/pi-ai": ">=0.80.2",
60
- "@earendil-works/pi-coding-agent": ">=0.80.2",
61
- "@earendil-works/pi-tui": ">=0.80.2",
58
+ "@earendil-works/pi-agent-core": ">=0.80.6",
59
+ "@earendil-works/pi-ai": ">=0.80.6",
60
+ "@earendil-works/pi-coding-agent": ">=0.80.6",
61
+ "@earendil-works/pi-tui": ">=0.80.6",
62
62
  "typebox": ">=1.1.24"
63
63
  },
64
64
  "lint-staged": {