@diegopetrucci/pi-librarian 0.1.0 → 0.1.2
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 +16 -5
- package/index.ts +156 -26
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# librarian
|
|
2
2
|
|
|
3
|
-
A pi GitHub research scout inspired by `pi-librarian`, with
|
|
3
|
+
A pi GitHub research scout inspired by `pi-librarian`, with a local checkout cache enabled by default.
|
|
4
4
|
|
|
5
|
-
When the `librarian` tool runs, it
|
|
5
|
+
When the `librarian` tool runs, it can cache/reuse repository checkouts locally. Use `/librarian-cache off` to force GitHub API/search and temporary fetched files only, or `/librarian-cache on` to re-enable cached local checkouts.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -35,9 +35,20 @@ Then reload pi:
|
|
|
35
35
|
- Tool name: `librarian`
|
|
36
36
|
- Uses a restricted subagent with `bash` and `read`
|
|
37
37
|
- Uses `gh` for GitHub search/API access
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- Cached repos are removed lazily after
|
|
38
|
+
- Uses cached local checkouts by default
|
|
39
|
+
- Toggle cache behavior for future calls with `/librarian-cache on | off | toggle | status`
|
|
40
|
+
- Cached repos are removed lazily after 7 days without use
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
/librarian-cache status
|
|
46
|
+
/librarian-cache off
|
|
47
|
+
/librarian-cache on
|
|
48
|
+
/librarian-cache toggle
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The command works in interactive mode, RPC mode, and print/JSON mode. It writes a global preference to `~/.pi/agent/extensions/librarian.json`, so separate non-UI invocations use the same setting. In non-UI modes, command feedback is written to stderr so stdout remains usable for normal output or JSON events.
|
|
41
52
|
|
|
42
53
|
## Cache location
|
|
43
54
|
|
package/index.ts
CHANGED
|
@@ -21,15 +21,18 @@ const MAX_TURNS = 10;
|
|
|
21
21
|
const MAX_TOOL_CALLS_TO_KEEP = 80;
|
|
22
22
|
const DEFAULT_BASH_TIMEOUT_SECONDS = 60;
|
|
23
23
|
const MAX_RUN_MS = 10 * 60 * 1000;
|
|
24
|
-
const CACHE_TTL_DAYS =
|
|
24
|
+
const CACHE_TTL_DAYS = 7;
|
|
25
25
|
const CACHE_TTL_MS = CACHE_TTL_DAYS * 24 * 60 * 60 * 1000;
|
|
26
26
|
const CACHE_METADATA_FILE = ".pi-librarian-cache.json";
|
|
27
27
|
const CACHE_MARKER_FILE = ".pi-librarian-cache-used";
|
|
28
|
+
const CACHE_CONFIG_FILE = "librarian.json";
|
|
28
29
|
|
|
29
30
|
type LibrarianStatus = "running" | "done" | "error" | "aborted";
|
|
30
31
|
|
|
31
32
|
type CacheMode = "disabled" | "enabled";
|
|
32
33
|
|
|
34
|
+
const DEFAULT_CACHE_MODE: CacheMode = "enabled";
|
|
35
|
+
|
|
33
36
|
type ToolCall = {
|
|
34
37
|
id: string;
|
|
35
38
|
name: string;
|
|
@@ -252,30 +255,72 @@ async function cleanupExpiredCache(cacheRoot: string): Promise<{ deleted: number
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
function getCacheConfigPath(): string {
|
|
259
|
+
return path.join(getAgentDir(), "extensions", CACHE_CONFIG_FILE);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseCacheMode(value: unknown): CacheMode | undefined {
|
|
263
|
+
if (typeof value === "string") {
|
|
264
|
+
const normalized = value.trim().toLowerCase();
|
|
265
|
+
if (normalized === "enabled" || normalized === "on" || normalized === "true") return "enabled";
|
|
266
|
+
if (normalized === "disabled" || normalized === "off" || normalized === "false") return "disabled";
|
|
267
|
+
}
|
|
268
|
+
if (value === true) return "enabled";
|
|
269
|
+
if (value === false) return "disabled";
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
257
272
|
|
|
273
|
+
async function readCachePreference(): Promise<CacheMode> {
|
|
258
274
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
const raw = await fs.readFile(getCacheConfigPath(), "utf8");
|
|
276
|
+
const parsed = JSON.parse(raw) as {
|
|
277
|
+
cacheMode?: unknown;
|
|
278
|
+
cacheEnabled?: unknown;
|
|
279
|
+
cache?: { mode?: unknown; enabled?: unknown };
|
|
280
|
+
};
|
|
281
|
+
return (
|
|
282
|
+
parseCacheMode(parsed.cacheMode) ??
|
|
283
|
+
parseCacheMode(parsed.cache?.mode) ??
|
|
284
|
+
parseCacheMode(parsed.cacheEnabled) ??
|
|
285
|
+
parseCacheMode(parsed.cache?.enabled) ??
|
|
286
|
+
DEFAULT_CACHE_MODE
|
|
270
287
|
);
|
|
271
|
-
|
|
272
|
-
return enabled
|
|
273
|
-
? { enabled: true, reason: "user opted into cached local checkouts" }
|
|
274
|
-
: { enabled: false, reason: "user declined or prompt timed out" };
|
|
275
288
|
} catch (error) {
|
|
276
|
-
|
|
277
|
-
|
|
289
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT" ? DEFAULT_CACHE_MODE : "disabled";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
294
|
+
const configPath = getCacheConfigPath();
|
|
295
|
+
const config = {
|
|
296
|
+
cacheMode: preference,
|
|
297
|
+
cacheEnabled: preference === "enabled",
|
|
298
|
+
updatedAt: new Date().toISOString(),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
302
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
|
|
306
|
+
if (preference === "enabled") {
|
|
307
|
+
return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { enabled: false, reason: "cache preference disabled; using GitHub API/temp files only" };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function formatCachePreference(preference: CacheMode): string {
|
|
314
|
+
return preference === "enabled" ? "on" : "off";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function notifyCommand(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
318
|
+
if (ctx.hasUI) {
|
|
319
|
+
ctx.ui.notify(message, type);
|
|
320
|
+
return;
|
|
278
321
|
}
|
|
322
|
+
|
|
323
|
+
console.error(message);
|
|
279
324
|
}
|
|
280
325
|
|
|
281
326
|
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
@@ -284,7 +329,12 @@ function resolveToolPath(cwd: string, rawPath: string): string {
|
|
|
284
329
|
}
|
|
285
330
|
|
|
286
331
|
function getBlockedBashReason(command: string, options: { workspace: string; cacheRoot: string; cacheEnabled: boolean }): string | undefined {
|
|
287
|
-
|
|
332
|
+
if (!options.cacheEnabled && command.includes(options.cacheRoot)) {
|
|
333
|
+
return "Local repo checkout cache is disabled for this Librarian call.";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let scan = command.split(options.workspace).join("<WORKSPACE>");
|
|
337
|
+
if (options.cacheEnabled) scan = scan.split(options.cacheRoot).join("<CACHE>");
|
|
288
338
|
if (/(^|[\n;&|()])\s*\//.test(scan)) return "Librarian bash blocks absolute-path executables.";
|
|
289
339
|
|
|
290
340
|
const destructiveLocal = /(^|[\n;&|()])\s*(?:sudo|su|rm|rmdir|mv|cp|chmod|chown|dd|truncate|killall|pkill|launchctl|osascript|pbcopy|pbpaste|eval|exec|xargs)(?=$|[\s;&|()])/;
|
|
@@ -438,13 +488,81 @@ function isAbortLikeError(error: unknown): boolean {
|
|
|
438
488
|
}
|
|
439
489
|
|
|
440
490
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
491
|
+
let cachePreference: CacheMode = DEFAULT_CACHE_MODE;
|
|
492
|
+
|
|
493
|
+
pi.on("session_start", async () => {
|
|
494
|
+
cachePreference = await readCachePreference();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
pi.registerCommand("librarian-cache", {
|
|
498
|
+
description: "Toggle Librarian local checkout cache for future librarian calls",
|
|
499
|
+
getArgumentCompletions: (prefix) => {
|
|
500
|
+
const commands = ["on", "off", "toggle", "status"];
|
|
501
|
+
const query = prefix.trim().toLowerCase();
|
|
502
|
+
const matches = commands.filter((command) => command.startsWith(query));
|
|
503
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
504
|
+
},
|
|
505
|
+
handler: async (args, ctx) => {
|
|
506
|
+
const action = args.trim().toLowerCase() || "status";
|
|
507
|
+
const cacheRoot = getUserCacheReposRoot();
|
|
508
|
+
const configPath = getCacheConfigPath();
|
|
509
|
+
|
|
510
|
+
const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
|
|
511
|
+
cachePreference = mode;
|
|
512
|
+
try {
|
|
513
|
+
await writeCachePreference(mode);
|
|
514
|
+
return undefined;
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const formatSetMessage = (mode: CacheMode, warning?: string): string => {
|
|
522
|
+
const main = mode === "enabled"
|
|
523
|
+
? `Librarian cache enabled. Future librarian calls will reuse local checkouts under ${cacheRoot}.`
|
|
524
|
+
: "Librarian cache disabled. Future librarian calls will use GitHub API/search and temporary fetched files only.";
|
|
525
|
+
return warning ? `${main} ${warning}` : main;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
if (action === "on" || action === "enable") {
|
|
529
|
+
const warning = await setPreference("enabled");
|
|
530
|
+
notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (action === "off" || action === "disable") {
|
|
535
|
+
const warning = await setPreference("disabled");
|
|
536
|
+
notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (action === "toggle") {
|
|
541
|
+
const next = cachePreference === "enabled" ? "disabled" : "enabled";
|
|
542
|
+
const warning = await setPreference(next);
|
|
543
|
+
notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (action === "status") {
|
|
548
|
+
notifyCommand(
|
|
549
|
+
ctx,
|
|
550
|
+
`Librarian cache is ${formatCachePreference(cachePreference)}. Cache directory: ${cacheRoot}. Config: ${configPath}. Repos unused for ${CACHE_TTL_DAYS} days are removed lazily.`,
|
|
551
|
+
);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
notifyCommand(ctx, "Usage: /librarian-cache on | off | toggle | status", "warning");
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
441
559
|
pi.registerTool({
|
|
442
560
|
name: "librarian",
|
|
443
561
|
label: "Librarian",
|
|
444
562
|
description:
|
|
445
|
-
"GitHub research scout for coding and personal-assistant tasks. Use when the answer likely lives in GitHub repos, exact repo/path locations are unknown, or you'd otherwise do exploratory gh search/tree probes plus local rg/read inspection. Librarian
|
|
563
|
+
"GitHub research scout for coding and personal-assistant tasks. Use when the answer likely lives in GitHub repos, exact repo/path locations are unknown, or you'd otherwise do exploratory gh search/tree probes plus local rg/read inspection. Librarian uses an optional 7-day local checkout cache by default; toggle it with /librarian-cache.",
|
|
446
564
|
promptSnippet:
|
|
447
|
-
"Research GitHub repositories with evidence-first path and line citations;
|
|
565
|
+
"Research GitHub repositories with evidence-first path and line citations; local checkout cache is enabled by default and user-toggleable with /librarian-cache.",
|
|
448
566
|
promptGuidelines: [
|
|
449
567
|
"Use librarian when the answer likely requires exploratory GitHub repository search or line-cited evidence from external repos.",
|
|
450
568
|
"Do not use librarian for files already present in the current workspace unless the user asks for external GitHub research.",
|
|
@@ -472,9 +590,21 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
472
590
|
await fs.mkdir(path.join(workspace, "repos"), { recursive: true });
|
|
473
591
|
|
|
474
592
|
const cacheRoot = getUserCacheReposRoot();
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
593
|
+
let cacheDecision = resolveCacheDecision(cachePreference);
|
|
594
|
+
let cleanup: { deleted: number; errors: string[] } = { deleted: 0, errors: [] };
|
|
595
|
+
if (cacheDecision.enabled) {
|
|
596
|
+
try {
|
|
597
|
+
await fs.mkdir(cacheRoot, { recursive: true });
|
|
598
|
+
cleanup = await cleanupExpiredCache(cacheRoot);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
601
|
+
cacheDecision = {
|
|
602
|
+
enabled: false,
|
|
603
|
+
reason: `cache setup failed (${message}); using GitHub API/temp files only`,
|
|
604
|
+
};
|
|
605
|
+
cleanup = { deleted: 0, errors: [`cache setup: ${message}`] };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
478
608
|
|
|
479
609
|
const details: LibrarianDetails = {
|
|
480
610
|
status: "running",
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-librarian",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A pi GitHub research scout
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"github",
|
|
9
|
+
"research",
|
|
10
|
+
"subagent",
|
|
11
|
+
"cache"
|
|
12
|
+
],
|
|
6
13
|
"license": "MIT",
|
|
7
14
|
"repository": {
|
|
8
15
|
"type": "git",
|