@diegopetrucci/pi-librarian 0.1.4 → 0.1.6
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 +14 -3
- package/index.ts +414 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# librarian
|
|
2
2
|
|
|
3
|
-
A pi GitHub research scout inspired by `pi-librarian`, with a local checkout cache
|
|
3
|
+
A pi GitHub research scout inspired by `pi-librarian`, with a local checkout cache disabled by default.
|
|
4
4
|
|
|
5
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
|
+
The internal librarian subagent uses a fast auto-selected model by default, requests `low` thinking, and prompts its scout to batch independent GitHub probes in parallel. Use `/librarian-config` to set a persistent internal model or thinking-level preference.
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
### Standalone npm package
|
|
@@ -35,8 +37,9 @@ Then reload pi:
|
|
|
35
37
|
- Tool name: `librarian`
|
|
36
38
|
- Uses a restricted subagent with `bash` and `read`
|
|
37
39
|
- Uses `gh` for GitHub search/API access
|
|
38
|
-
- Uses cached local checkouts
|
|
40
|
+
- Uses cached local checkouts only when enabled
|
|
39
41
|
- Toggle cache behavior for future calls with `/librarian-cache on | off | toggle | status`
|
|
42
|
+
- Configure internal subagent defaults with `/librarian-config status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]`
|
|
40
43
|
- Cached repos are removed lazily after 7 days without use
|
|
41
44
|
|
|
42
45
|
## Commands
|
|
@@ -48,7 +51,15 @@ Then reload pi:
|
|
|
48
51
|
/librarian-cache toggle
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
```text
|
|
55
|
+
/librarian-config status
|
|
56
|
+
/librarian-config model auto
|
|
57
|
+
/librarian-config model anthropic/claude-haiku-4-5:medium
|
|
58
|
+
/librarian-config thinking low
|
|
59
|
+
/librarian-config clear model
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The commands work in interactive mode, RPC mode, and print/JSON mode. They write global preferences to `~/.pi/agent/extensions/librarian.json`, so separate non-UI invocations use the same settings. In non-UI modes, command feedback is written to stderr so stdout remains usable for normal output or JSON events.
|
|
52
63
|
|
|
53
64
|
## Cache location
|
|
54
65
|
|
package/index.ts
CHANGED
|
@@ -30,8 +30,22 @@ const CACHE_CONFIG_FILE = "librarian.json";
|
|
|
30
30
|
type LibrarianStatus = "running" | "done" | "error" | "aborted";
|
|
31
31
|
|
|
32
32
|
type CacheMode = "disabled" | "enabled";
|
|
33
|
+
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
33
34
|
|
|
34
|
-
const DEFAULT_CACHE_MODE: CacheMode = "
|
|
35
|
+
const DEFAULT_CACHE_MODE: CacheMode = "disabled";
|
|
36
|
+
const DEFAULT_THINKING_LEVEL: ThinkingLevel = "low";
|
|
37
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
38
|
+
|
|
39
|
+
const PREFERRED_FAST_MODEL_PATTERNS = [
|
|
40
|
+
/\bgpt[-_. ]?5\.5(?:[-_. ].*)?\b(?:mini|nano|fast|lite)\b/,
|
|
41
|
+
/\bgpt[-_. ]?5(?:[-_. ].*)?\b(?:mini|nano|fast|lite)\b/,
|
|
42
|
+
/\bgpt[-_. ]?4(?:\.1|o)?(?:[-_. ].*)?\b(?:mini|nano)\b/,
|
|
43
|
+
/\bgemini\b.*\b(?:flash|flash-lite|lite)\b/,
|
|
44
|
+
/\bclaude\b.*\bhaiku\b/,
|
|
45
|
+
/\b(?:mistral|codestral)\b.*\b(?:small|mini|lite)\b/,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const HEAVY_MODEL_PATTERN = /\b(?:opus|pro|ultra|max)\b/;
|
|
35
49
|
|
|
36
50
|
type ToolCall = {
|
|
37
51
|
id: string;
|
|
@@ -51,10 +65,20 @@ type CacheDetails = {
|
|
|
51
65
|
decisionReason: string;
|
|
52
66
|
};
|
|
53
67
|
|
|
68
|
+
type LibrarianModelDetails = {
|
|
69
|
+
modelRef: string;
|
|
70
|
+
modelId: string;
|
|
71
|
+
provider: string;
|
|
72
|
+
thinkingLevel: ThinkingLevel;
|
|
73
|
+
autoSelected: boolean;
|
|
74
|
+
selectionReason: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
54
77
|
type LibrarianDetails = {
|
|
55
78
|
status: LibrarianStatus;
|
|
56
79
|
workspace: string;
|
|
57
80
|
cache: CacheDetails;
|
|
81
|
+
model: LibrarianModelDetails;
|
|
58
82
|
turns: number;
|
|
59
83
|
toolCalls: ToolCall[];
|
|
60
84
|
startedAt: number;
|
|
@@ -91,6 +115,16 @@ const LibrarianParams = Type.Object({
|
|
|
91
115
|
default: DEFAULT_MAX_SEARCH_RESULTS,
|
|
92
116
|
}),
|
|
93
117
|
),
|
|
118
|
+
model: Type.Optional(
|
|
119
|
+
Type.String({
|
|
120
|
+
description: "Optional model override for the internal librarian subagent. Use provider/model or auto.",
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
thinkingLevel: Type.Optional(
|
|
124
|
+
Type.String({
|
|
125
|
+
description: `Optional thinking override for the internal librarian subagent (${THINKING_LEVELS.join(" | ")}). Default ${DEFAULT_THINKING_LEVEL}.`,
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
94
128
|
});
|
|
95
129
|
|
|
96
130
|
function asStringArray(value: unknown, maxItems = 30): string[] {
|
|
@@ -259,6 +293,12 @@ function getCacheConfigPath(): string {
|
|
|
259
293
|
return path.join(getAgentDir(), "extensions", CACHE_CONFIG_FILE);
|
|
260
294
|
}
|
|
261
295
|
|
|
296
|
+
type LibrarianPreferences = {
|
|
297
|
+
cacheMode: CacheMode;
|
|
298
|
+
model?: string;
|
|
299
|
+
thinkingLevel: ThinkingLevel;
|
|
300
|
+
};
|
|
301
|
+
|
|
262
302
|
function parseCacheMode(value: unknown): CacheMode | undefined {
|
|
263
303
|
if (typeof value === "string") {
|
|
264
304
|
const normalized = value.trim().toLowerCase();
|
|
@@ -270,31 +310,74 @@ function parseCacheMode(value: unknown): CacheMode | undefined {
|
|
|
270
310
|
return undefined;
|
|
271
311
|
}
|
|
272
312
|
|
|
273
|
-
|
|
313
|
+
function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
314
|
+
if (typeof value !== "string") return undefined;
|
|
315
|
+
const normalized = value.trim().toLowerCase();
|
|
316
|
+
return (THINKING_LEVELS as readonly string[]).includes(normalized) ? (normalized as ThinkingLevel) : undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function normalizeModelPreference(value: unknown): string | undefined {
|
|
320
|
+
if (typeof value !== "string") return undefined;
|
|
321
|
+
const trimmed = value.trim();
|
|
322
|
+
const normalized = trimmed.toLowerCase();
|
|
323
|
+
if (!trimmed || normalized === "auto" || normalized === "current") return undefined;
|
|
324
|
+
return trimmed;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseModelPreference(value: unknown): { model?: string; thinkingLevel?: ThinkingLevel } {
|
|
328
|
+
const model = normalizeModelPreference(value);
|
|
329
|
+
if (!model) return {};
|
|
330
|
+
const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
|
|
331
|
+
if (!match?.[1]) return { model };
|
|
332
|
+
const baseModel = match[1].trim();
|
|
333
|
+
if (!baseModel || baseModel.toLowerCase() === "auto" || baseModel.toLowerCase() === "current") {
|
|
334
|
+
return { thinkingLevel: parseThinkingLevel(match[2]) };
|
|
335
|
+
}
|
|
336
|
+
return { model: baseModel, thinkingLevel: parseThinkingLevel(match[2]) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function readLibrarianPreferences(): Promise<LibrarianPreferences> {
|
|
274
340
|
try {
|
|
275
341
|
const raw = await fs.readFile(getCacheConfigPath(), "utf8");
|
|
276
342
|
const parsed = JSON.parse(raw) as {
|
|
277
343
|
cacheMode?: unknown;
|
|
278
344
|
cacheEnabled?: unknown;
|
|
279
345
|
cache?: { mode?: unknown; enabled?: unknown };
|
|
346
|
+
model?: unknown;
|
|
347
|
+
defaultModel?: unknown;
|
|
348
|
+
thinkingLevel?: unknown;
|
|
349
|
+
defaultThinkingLevel?: unknown;
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
cacheMode:
|
|
353
|
+
parseCacheMode(parsed.cacheMode) ??
|
|
354
|
+
parseCacheMode(parsed.cache?.mode) ??
|
|
355
|
+
parseCacheMode(parsed.cacheEnabled) ??
|
|
356
|
+
parseCacheMode(parsed.cache?.enabled) ??
|
|
357
|
+
DEFAULT_CACHE_MODE,
|
|
358
|
+
model: parseModelPreference(parsed.model).model ?? parseModelPreference(parsed.defaultModel).model,
|
|
359
|
+
thinkingLevel:
|
|
360
|
+
parseThinkingLevel(parsed.thinkingLevel) ??
|
|
361
|
+
parseThinkingLevel(parsed.defaultThinkingLevel) ??
|
|
362
|
+
parseModelPreference(parsed.model).thinkingLevel ??
|
|
363
|
+
parseModelPreference(parsed.defaultModel).thinkingLevel ??
|
|
364
|
+
DEFAULT_THINKING_LEVEL,
|
|
280
365
|
};
|
|
281
|
-
return (
|
|
282
|
-
parseCacheMode(parsed.cacheMode) ??
|
|
283
|
-
parseCacheMode(parsed.cache?.mode) ??
|
|
284
|
-
parseCacheMode(parsed.cacheEnabled) ??
|
|
285
|
-
parseCacheMode(parsed.cache?.enabled) ??
|
|
286
|
-
DEFAULT_CACHE_MODE
|
|
287
|
-
);
|
|
288
366
|
} catch (error) {
|
|
289
|
-
return
|
|
367
|
+
return {
|
|
368
|
+
cacheMode: (error as NodeJS.ErrnoException).code === "ENOENT" ? DEFAULT_CACHE_MODE : "disabled",
|
|
369
|
+
thinkingLevel: DEFAULT_THINKING_LEVEL,
|
|
370
|
+
};
|
|
290
371
|
}
|
|
291
372
|
}
|
|
292
373
|
|
|
293
|
-
async function
|
|
374
|
+
async function writeLibrarianPreferences(preferences: LibrarianPreferences): Promise<void> {
|
|
294
375
|
const configPath = getCacheConfigPath();
|
|
295
376
|
const config = {
|
|
296
|
-
cacheMode:
|
|
297
|
-
cacheEnabled:
|
|
377
|
+
cacheMode: preferences.cacheMode,
|
|
378
|
+
cacheEnabled: preferences.cacheMode === "enabled",
|
|
379
|
+
...(preferences.model ? { model: preferences.model } : {}),
|
|
380
|
+
thinkingLevel: preferences.thinkingLevel,
|
|
298
381
|
updatedAt: new Date().toISOString(),
|
|
299
382
|
};
|
|
300
383
|
|
|
@@ -302,6 +385,11 @@ async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
|
302
385
|
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
303
386
|
}
|
|
304
387
|
|
|
388
|
+
async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
389
|
+
const preferences = await readLibrarianPreferences();
|
|
390
|
+
await writeLibrarianPreferences({ ...preferences, cacheMode: preference });
|
|
391
|
+
}
|
|
392
|
+
|
|
305
393
|
function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
|
|
306
394
|
if (preference === "enabled") {
|
|
307
395
|
return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
|
|
@@ -314,6 +402,10 @@ function formatCachePreference(preference: CacheMode): string {
|
|
|
314
402
|
return preference === "enabled" ? "on" : "off";
|
|
315
403
|
}
|
|
316
404
|
|
|
405
|
+
function formatLibrarianPreferences(preferences: LibrarianPreferences): string {
|
|
406
|
+
return `Librarian defaults: cache=${formatCachePreference(preferences.cacheMode)}, model=${preferences.model ?? "auto"}, thinkingLevel=${preferences.thinkingLevel}. Config: ${getCacheConfigPath()}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
317
409
|
function notifyCommand(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
318
410
|
if (ctx.hasUI) {
|
|
319
411
|
ctx.ui.notify(message, type);
|
|
@@ -323,6 +415,117 @@ function notifyCommand(ctx: ExtensionContext, message: string, type: "info" | "w
|
|
|
323
415
|
console.error(message);
|
|
324
416
|
}
|
|
325
417
|
|
|
418
|
+
function modelRef(model: any): string {
|
|
419
|
+
return `${model.provider}/${model.id}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function modelCostScore(model: any): number {
|
|
423
|
+
const cost = model.cost ?? {};
|
|
424
|
+
const input = typeof cost.input === "number" ? cost.input : 0;
|
|
425
|
+
const output = typeof cost.output === "number" ? cost.output : 0;
|
|
426
|
+
return input + output;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function rankLibrarianModel(model: any): number {
|
|
430
|
+
const text = `${model.id ?? ""} ${model.name ?? ""}`.toLowerCase();
|
|
431
|
+
let score = modelCostScore(model) * 1_000_000;
|
|
432
|
+
if (model.reasoning) score += 50;
|
|
433
|
+
if (/\b(?:mini|nano|haiku|flash|lite|small|fast|instant)\b/.test(text)) score -= 10;
|
|
434
|
+
if (HEAVY_MODEL_PATTERN.test(text)) score += 1_000;
|
|
435
|
+
if ((model.contextWindow ?? 0) < 32_000) score += 100;
|
|
436
|
+
return score;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isPreferredFastLibrarianModel(model: any): boolean {
|
|
440
|
+
const text = `${model.provider ?? ""} ${model.id ?? ""} ${model.name ?? ""}`.toLowerCase();
|
|
441
|
+
if (HEAVY_MODEL_PATTERN.test(text)) return false;
|
|
442
|
+
return PREFERRED_FAST_MODEL_PATTERNS.some((pattern) => pattern.test(text));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function rankPreferredFastLibrarianModel(model: any): number {
|
|
446
|
+
const text = `${model.provider ?? ""} ${model.id ?? ""} ${model.name ?? ""}`.toLowerCase();
|
|
447
|
+
let score = rankLibrarianModel(model);
|
|
448
|
+
const patternIndex = PREFERRED_FAST_MODEL_PATTERNS.findIndex((pattern) => pattern.test(text));
|
|
449
|
+
if (patternIndex >= 0) score += patternIndex * 100;
|
|
450
|
+
return score;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function findAvailableModel(
|
|
454
|
+
ctx: { model?: any; modelRegistry: { getAvailable(): any[] | Promise<any[]> } },
|
|
455
|
+
modelPreference: string,
|
|
456
|
+
): Promise<any | undefined> {
|
|
457
|
+
const available = await ctx.modelRegistry.getAvailable();
|
|
458
|
+
const trimmed = modelPreference.trim();
|
|
459
|
+
const provider = trimmed.includes("/") ? trimmed.split("/")[0].toLowerCase() : ctx.model?.provider?.toLowerCase();
|
|
460
|
+
const id = trimmed.includes("/") ? trimmed.split("/").slice(1).join("/").toLowerCase() : trimmed.toLowerCase();
|
|
461
|
+
|
|
462
|
+
const exact = available.find(
|
|
463
|
+
(model) => model.id.toLowerCase() === id && (!provider || model.provider.toLowerCase() === provider),
|
|
464
|
+
);
|
|
465
|
+
if (exact) return exact;
|
|
466
|
+
|
|
467
|
+
const partial = available.find(
|
|
468
|
+
(model) => model.id.toLowerCase().includes(id) && (!provider || model.provider.toLowerCase() === provider),
|
|
469
|
+
);
|
|
470
|
+
if (partial) return partial;
|
|
471
|
+
|
|
472
|
+
if (!provider) {
|
|
473
|
+
const uniqueById = available.filter((model) => model.id.toLowerCase() === id);
|
|
474
|
+
if (uniqueById.length === 1) return uniqueById[0];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function selectLibrarianModel(
|
|
481
|
+
ctx: { model?: any; modelRegistry: { getAvailable(): any[] | Promise<any[]> } },
|
|
482
|
+
modelPreference: string | undefined,
|
|
483
|
+
thinkingLevel: ThinkingLevel,
|
|
484
|
+
): Promise<{ model: any; details: LibrarianModelDetails }> {
|
|
485
|
+
const normalized = modelPreference?.trim();
|
|
486
|
+
if (normalized) {
|
|
487
|
+
const matched = await findAvailableModel(ctx, normalized);
|
|
488
|
+
if (matched) {
|
|
489
|
+
return {
|
|
490
|
+
model: matched,
|
|
491
|
+
details: {
|
|
492
|
+
modelRef: modelRef(matched),
|
|
493
|
+
provider: matched.provider,
|
|
494
|
+
modelId: matched.id,
|
|
495
|
+
thinkingLevel,
|
|
496
|
+
autoSelected: false,
|
|
497
|
+
selectionReason: "Using the configured librarian model.",
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const available = await ctx.modelRegistry.getAvailable();
|
|
504
|
+
const preferred = available.filter(isPreferredFastLibrarianModel);
|
|
505
|
+
const winner = preferred.length > 0
|
|
506
|
+
? [...preferred].sort((a, b) => rankPreferredFastLibrarianModel(a) - rankPreferredFastLibrarianModel(b))[0]
|
|
507
|
+
: [...available].sort((a, b) => rankLibrarianModel(a) - rankLibrarianModel(b))[0];
|
|
508
|
+
if (!winner) {
|
|
509
|
+
throw new Error("No authenticated models are available for Librarian. Log in or configure an API key first.");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const fallbackText = normalized ? ` Configured model ${normalized} was unavailable, so Librarian fell back to auto-selection.` : "";
|
|
513
|
+
const selectionReason = preferred.length > 0
|
|
514
|
+
? `Selected a preferred fast Librarian model.${fallbackText}`
|
|
515
|
+
: `Selected the cheapest available model because no preferred fast Librarian model was available.${fallbackText}`;
|
|
516
|
+
return {
|
|
517
|
+
model: winner,
|
|
518
|
+
details: {
|
|
519
|
+
modelRef: modelRef(winner),
|
|
520
|
+
provider: winner.provider,
|
|
521
|
+
modelId: winner.id,
|
|
522
|
+
thinkingLevel,
|
|
523
|
+
autoSelected: true,
|
|
524
|
+
selectionReason,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
326
529
|
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
327
530
|
const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
|
|
328
531
|
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
@@ -436,7 +639,7 @@ function buildSystemPrompt(options: {
|
|
|
436
639
|
? `\nLocal checkout cache is ENABLED for this call.\n- Cache root: ${options.cacheRoot}\n- Use checkout path pattern: ${options.cacheRoot}/github.com/<owner>/<repo>\n- Reuse an existing checkout when it has a .git directory. Fetch/prune before relying on it: git -C "$DIR" fetch --all --prune --tags --quiet\n- If missing, clone with gh repo clone "$REPO" "$DIR" (or git clone https://github.com/$REPO.git "$DIR").\n- If a ref/branch/SHA is requested, fetch it and check it out locally before citing files from that ref.\n- After using a checkout, update its cache marker: touch "$DIR/${CACHE_MARKER_FILE}"\n- Prefer local rg/read inside cached checkouts once a repo is cloned, and cite absolute cached paths with line ranges.\n- Clone only repositories that are relevant to the query; do not bulk-clone broad owner/org scopes unless necessary.`
|
|
437
640
|
: `\nLocal checkout cache is DISABLED for this call.\n- Do not clone repositories.\n- Use gh search/API/tree/contents calls and cache only necessary proof files under ${options.workspace}/repos/<owner>/<repo>/<path>.`;
|
|
438
641
|
|
|
439
|
-
return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse only the available bash/read tools. Use gh, jq, rg, find/fd, ls, stat, mkdir, base64, and nl -ba for GitHub reconnaissance and numbered evidence. Use read for focused local file inspection.\n\nWorkspace: ${options.workspace}\nDefault gh search limit: ${options.maxSearchResults}\nTurn budget: ${MAX_TURNS} turns total, including your final answer. Stop searching once you have enough evidence.\n${cacheSection}\n\nNon-negotiable constraints:\n- Never treat gh search snippets as proof by themselves. Use fetched files or local checkouts for code-content claims.\n- Keep temporary workspace writes under ${options.workspace}/repos unless local checkout cache is enabled, in which case writes under the cache root are also allowed.
|
|
642
|
+
return `You are Librarian, an evidence-first GitHub code scout running inside pi.\n\nUse only the available bash/read tools. Use gh, jq, rg, find/fd, ls, stat, mkdir, base64, and nl -ba for GitHub reconnaissance and numbered evidence. Use read for focused local file inspection.\n\nWorkspace: ${options.workspace}\nDefault gh search limit: ${options.maxSearchResults}\nTurn budget: ${MAX_TURNS} turns total, including your final answer. Stop searching once you have enough evidence.\n${cacheSection}\n\nPerformance guidance:\n- Prefer fast, parallel exploration. When probes are independent, issue multiple separate tool calls in the same assistant turn instead of waiting for each result one-by-one.\n- Aim for 4-8 independent bash/read calls in an exploration turn when useful: parallel gh searches, tree probes, contents fetches, and targeted file reads.\n- Keep dependent work sequential, and avoid concurrent writes to the same file, checkout, or cache directory.\n\nNon-negotiable constraints:\n- Never treat gh search snippets as proof by themselves. Use fetched files or local checkouts for code-content claims.\n- Keep temporary workspace writes under ${options.workspace}/repos unless local checkout cache is enabled, in which case writes under the cache root are also allowed.
|
|
440
643
|
- A runtime guard blocks destructive shell commands, credential/environment inspection, and reads outside the workspace/cache.\n- Never paste whole files. Use short snippets only when they clarify the evidence.\n- If evidence is partial or access fails (404/403), state the limitation clearly.\n- Do not present anything as fact unless it appeared in tool output or in a file you read.\n\nRecommended search flow:\n1. If symbols/text are known, start with gh search code and the provided repo/owner filters.\n2. If a repo is known but paths are unclear, resolve the default branch and inspect the git tree or contents API.\n3. Fetch or clone only the files/repos required to prove the answer.\n4. Use rg/read/nl -ba locally to produce stable path and line evidence.\n\nUseful gh patterns:\n- gh repo view "$REPO" --json defaultBranchRef --jq '.defaultBranchRef.name'\n- gh search code '<terms>' --json path,repository,sha,url,textMatches --limit ${options.maxSearchResults}\n- gh api "repos/$REPO/git/trees/$REF?recursive=1" > tree.json\n- gh api "repos/$REPO/contents/$FILE?ref=$REF" --jq .content | tr -d '\\n' | base64 --decode > "repos/$REPO/$FILE"\n- rg -n '<pattern>' '<local path>'\n- nl -ba '<local file>' | sed -n '10,30p'\n\nOutput format, exact order:\n## Summary\n1-3 concise sentences.\n## Locations\n- \`path\` or \`path:lineStart-lineEnd\` — what is here and why it matters; include GitHub URL when useful. If nothing relevant is found, write \`- (none)\`.\n## Evidence\n- \`path\` or \`path:lineStart-lineEnd\` — what this proves. Include concise snippets only if useful.\n## Searched\nOnly include when incomplete/not found or when the search path matters. List queries, filters, and probes used.\n## Next steps\nOptional: 1-3 narrow follow-up checks for remaining ambiguity.`;
|
|
441
644
|
}
|
|
442
645
|
|
|
@@ -444,20 +647,56 @@ function buildUserPrompt(query: string, repos: string[], owners: string[], maxSe
|
|
|
444
647
|
return `Task: locate and cite exact GitHub code locations that answer the query.\n\nQuery: ${query}\nRepository filters: ${repos.length ? repos.join(", ") : "(none)"}\nOwner filters: ${owners.length ? owners.join(", ") : "(none)"}\nMax search results per gh search call: ${maxSearchResults}\nLocal checkout cache: ${cache.mode === "enabled" ? `enabled at ${cache.root}` : "disabled"}\nCache decision: ${cache.decisionReason}\n\nRespond directly with concise, citation-heavy findings. Always pass --limit ${maxSearchResults} to gh search code unless a narrower command is clearly better.`;
|
|
445
648
|
}
|
|
446
649
|
|
|
447
|
-
|
|
650
|
+
type AssistantLikeMessage = {
|
|
651
|
+
role?: string;
|
|
652
|
+
content?: unknown;
|
|
653
|
+
stopReason?: unknown;
|
|
654
|
+
errorMessage?: unknown;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
function getLastAssistantMessage(messages: unknown[]): AssistantLikeMessage | undefined {
|
|
448
658
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
449
|
-
const message = messages[i] as
|
|
450
|
-
if (message?.role
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
659
|
+
const message = messages[i] as AssistantLikeMessage;
|
|
660
|
+
if (message?.role === "assistant") return message;
|
|
661
|
+
}
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function extractLastAssistantText(messages: unknown[]): string {
|
|
666
|
+
const message = getLastAssistantMessage(messages);
|
|
667
|
+
if (!message || !Array.isArray(message.content)) return "";
|
|
668
|
+
const parts: string[] = [];
|
|
669
|
+
for (const part of message.content) {
|
|
670
|
+
if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
|
|
671
|
+
const text = (part as { text?: unknown }).text;
|
|
672
|
+
if (typeof text === "string") parts.push(text);
|
|
457
673
|
}
|
|
458
|
-
if (parts.length) return parts.join("").trim();
|
|
459
674
|
}
|
|
460
|
-
return "";
|
|
675
|
+
return parts.join("").trim();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function describeNoAnswerReason(message: AssistantLikeMessage | undefined, turns: number): string {
|
|
679
|
+
if (!message) return "the internal subagent produced no assistant message";
|
|
680
|
+
if (message.stopReason === "error") {
|
|
681
|
+
const errorMessage = typeof message.errorMessage === "string" && message.errorMessage.trim()
|
|
682
|
+
? message.errorMessage.trim()
|
|
683
|
+
: "provider/model error";
|
|
684
|
+
return `the internal subagent stopped with an error: ${errorMessage}`;
|
|
685
|
+
}
|
|
686
|
+
if (message.stopReason === "aborted") return "the internal subagent was aborted before producing an answer";
|
|
687
|
+
if (turns >= MAX_TURNS) return `the internal subagent reached the ${MAX_TURNS}-turn budget without producing a final answer`;
|
|
688
|
+
const stopReason = typeof message.stopReason === "string" && message.stopReason.trim()
|
|
689
|
+
? ` (stopReason: ${message.stopReason.trim()})`
|
|
690
|
+
: "";
|
|
691
|
+
return `the internal subagent completed without final assistant text${stopReason}`;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function formatInternalFailure(details: LibrarianDetails, reason: string): string {
|
|
695
|
+
return [
|
|
696
|
+
`Internal librarian run failed: ${reason}.`,
|
|
697
|
+
`Diagnostics: status=${details.status}; model=${details.model.modelRef}; thinking=${details.model.thinkingLevel}; modelSelection=${details.model.autoSelected ? "auto" : "configured"}; turns=${details.turns}; toolCalls=${details.toolCalls.length}.`,
|
|
698
|
+
"No answer was produced. This is not a reliable \"no results found\" signal; retry later or configure a different Librarian model with /librarian-config.",
|
|
699
|
+
].join("\n");
|
|
461
700
|
}
|
|
462
701
|
|
|
463
702
|
function formatToolCall(call: ToolCall): string {
|
|
@@ -489,9 +728,119 @@ function isAbortLikeError(error: unknown): boolean {
|
|
|
489
728
|
|
|
490
729
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
491
730
|
let cachePreference: CacheMode = DEFAULT_CACHE_MODE;
|
|
731
|
+
let modelPreference: string | undefined;
|
|
732
|
+
let thinkingPreference: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
492
733
|
|
|
493
734
|
pi.on("session_start", async () => {
|
|
494
|
-
|
|
735
|
+
const preferences = await readLibrarianPreferences();
|
|
736
|
+
cachePreference = preferences.cacheMode;
|
|
737
|
+
modelPreference = preferences.model;
|
|
738
|
+
thinkingPreference = preferences.thinkingLevel;
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const currentPreferences = (): LibrarianPreferences => ({
|
|
742
|
+
cacheMode: cachePreference,
|
|
743
|
+
...(modelPreference ? { model: modelPreference } : {}),
|
|
744
|
+
thinkingLevel: thinkingPreference,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const savePreferences = async (preferences: LibrarianPreferences): Promise<string | undefined> => {
|
|
748
|
+
cachePreference = preferences.cacheMode;
|
|
749
|
+
modelPreference = preferences.model;
|
|
750
|
+
thinkingPreference = preferences.thinkingLevel;
|
|
751
|
+
try {
|
|
752
|
+
await writeLibrarianPreferences(preferences);
|
|
753
|
+
return undefined;
|
|
754
|
+
} catch (error) {
|
|
755
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
756
|
+
return `Preference changed for this process, but could not save ${getCacheConfigPath()}: ${message}`;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
pi.registerCommand("librarian-config", {
|
|
761
|
+
description: "Configure Librarian subagent model and thinking defaults",
|
|
762
|
+
getArgumentCompletions: (prefix) => {
|
|
763
|
+
const parts = prefix.trim().split(/\s+/).filter(Boolean);
|
|
764
|
+
if (parts.length <= 1) {
|
|
765
|
+
const commands = ["status", "model", "thinking", "clear"];
|
|
766
|
+
const query = parts[0]?.toLowerCase() ?? "";
|
|
767
|
+
return commands.filter((command) => command.startsWith(query)).map((value) => ({ value, label: value }));
|
|
768
|
+
}
|
|
769
|
+
if (parts[0]?.toLowerCase() === "thinking") {
|
|
770
|
+
const query = parts[1]?.toLowerCase() ?? "";
|
|
771
|
+
return [...THINKING_LEVELS, "auto"].filter((level) => level.startsWith(query)).map((value) => ({ value, label: value }));
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
},
|
|
775
|
+
handler: async (args, ctx) => {
|
|
776
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
777
|
+
const [command = "status", ...rest] = tokens;
|
|
778
|
+
const action = command.toLowerCase();
|
|
779
|
+
|
|
780
|
+
if (action === "status" || action === "show") {
|
|
781
|
+
notifyCommand(ctx, formatLibrarianPreferences(currentPreferences()));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (action === "model") {
|
|
786
|
+
const value = rest.join(" ").trim();
|
|
787
|
+
if (!value) {
|
|
788
|
+
notifyCommand(ctx, "Usage: /librarian-config model <provider/model|auto>", "warning");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const normalized = value.toLowerCase();
|
|
792
|
+
if (normalized === "current" || normalized.startsWith("current:")) {
|
|
793
|
+
notifyCommand(ctx, "Librarian model=current is no longer supported. Use /librarian-config model auto or choose an explicit provider/model.", "warning");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const parsedModel = parseModelPreference(value);
|
|
797
|
+
const next = normalized === "auto" || normalized === "clear" || normalized === "default"
|
|
798
|
+
? { ...currentPreferences(), model: undefined }
|
|
799
|
+
: {
|
|
800
|
+
...currentPreferences(),
|
|
801
|
+
model: parsedModel.model,
|
|
802
|
+
thinkingLevel: parsedModel.thinkingLevel ?? thinkingPreference,
|
|
803
|
+
};
|
|
804
|
+
const warning = await savePreferences(next);
|
|
805
|
+
notifyCommand(ctx, `Librarian model default updated. ${warning ?? formatLibrarianPreferences(currentPreferences())}`, warning ? "warning" : "info");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (action === "thinking" || action === "think" || action === "thinking-level") {
|
|
810
|
+
const value = rest[0]?.trim().toLowerCase();
|
|
811
|
+
if (!value) {
|
|
812
|
+
notifyCommand(ctx, `Usage: /librarian-config thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const thinkingLevel = value === "auto" || value === "clear" || value === "default"
|
|
816
|
+
? DEFAULT_THINKING_LEVEL
|
|
817
|
+
: parseThinkingLevel(value);
|
|
818
|
+
if (!thinkingLevel) {
|
|
819
|
+
notifyCommand(ctx, `Usage: /librarian-config thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const warning = await savePreferences({ ...currentPreferences(), thinkingLevel });
|
|
823
|
+
notifyCommand(ctx, `Librarian thinking default set to ${thinkingLevel}. ${warning ?? formatLibrarianPreferences(currentPreferences())}`, warning ? "warning" : "info");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (action === "clear" || action === "reset") {
|
|
828
|
+
const target = rest[0]?.trim().toLowerCase() || "all";
|
|
829
|
+
let next: LibrarianPreferences;
|
|
830
|
+
if (target === "all") next = { cacheMode: cachePreference, thinkingLevel: DEFAULT_THINKING_LEVEL };
|
|
831
|
+
else if (target === "model") next = { ...currentPreferences(), model: undefined };
|
|
832
|
+
else if (target === "thinking" || target === "thinking-level") next = { ...currentPreferences(), thinkingLevel: DEFAULT_THINKING_LEVEL };
|
|
833
|
+
else {
|
|
834
|
+
notifyCommand(ctx, "Usage: /librarian-config clear [all|model|thinking]", "warning");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const warning = await savePreferences(next);
|
|
838
|
+
notifyCommand(ctx, `Librarian defaults cleared (${target}). ${warning ?? formatLibrarianPreferences(currentPreferences())}`, warning ? "warning" : "info");
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
notifyCommand(ctx, "Usage: /librarian-config status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]", "warning");
|
|
843
|
+
},
|
|
495
844
|
});
|
|
496
845
|
|
|
497
846
|
pi.registerCommand("librarian-cache", {
|
|
@@ -556,16 +905,24 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
556
905
|
},
|
|
557
906
|
});
|
|
558
907
|
|
|
908
|
+
pi.on("tool_result", async (event) => {
|
|
909
|
+
if (event.toolName !== "librarian") return undefined;
|
|
910
|
+
const details = event.details as LibrarianDetails | undefined;
|
|
911
|
+
if (details?.status !== "error") return undefined;
|
|
912
|
+
return { isError: true };
|
|
913
|
+
});
|
|
914
|
+
|
|
559
915
|
pi.registerTool({
|
|
560
916
|
name: "librarian",
|
|
561
917
|
label: "Librarian",
|
|
562
918
|
description:
|
|
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.",
|
|
919
|
+
"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 that is disabled by default; toggle it with /librarian-cache. Configure its internal subagent defaults with /librarian-config.",
|
|
564
920
|
promptSnippet:
|
|
565
|
-
"Research GitHub repositories with evidence-first path and line citations; local checkout cache is
|
|
921
|
+
"Research GitHub repositories with evidence-first path and line citations; local checkout cache is disabled by default and user-toggleable with /librarian-cache. Internal subagent defaults are user-configurable with /librarian-config and default to low thinking.",
|
|
566
922
|
promptGuidelines: [
|
|
567
923
|
"Use librarian when the answer likely requires exploratory GitHub repository search or line-cited evidence from external repos.",
|
|
568
924
|
"Do not use librarian for files already present in the current workspace unless the user asks for external GitHub research.",
|
|
925
|
+
"Use model or thinkingLevel only when the user explicitly asks for a non-default internal librarian model or thinking level.",
|
|
569
926
|
],
|
|
570
927
|
parameters: LibrarianParams,
|
|
571
928
|
|
|
@@ -573,7 +930,6 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
573
930
|
const rawQuery = (params as { query?: unknown }).query;
|
|
574
931
|
const query = typeof rawQuery === "string" ? rawQuery.trim() : "";
|
|
575
932
|
if (!query) throw new Error("Invalid parameters: expected query to be a non-empty string.");
|
|
576
|
-
if (!ctx.model) throw new Error("Librarian needs an active pi model, but ctx.model is unavailable.");
|
|
577
933
|
|
|
578
934
|
const repos = asStringArray((params as { repos?: unknown }).repos);
|
|
579
935
|
const owners = asStringArray((params as { owners?: unknown }).owners);
|
|
@@ -583,6 +939,12 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
583
939
|
MAX_SEARCH_RESULTS,
|
|
584
940
|
DEFAULT_MAX_SEARCH_RESULTS,
|
|
585
941
|
);
|
|
942
|
+
const explicitModel = parseModelPreference((params as { model?: unknown }).model);
|
|
943
|
+
const thinkingLevel =
|
|
944
|
+
parseThinkingLevel((params as { thinkingLevel?: unknown }).thinkingLevel) ??
|
|
945
|
+
explicitModel.thinkingLevel ??
|
|
946
|
+
thinkingPreference;
|
|
947
|
+
const selectedModel = await selectLibrarianModel(ctx, explicitModel.model ?? modelPreference, thinkingLevel);
|
|
586
948
|
|
|
587
949
|
const workspaceBase = path.join(os.tmpdir(), "pi-librarian");
|
|
588
950
|
await fs.mkdir(workspaceBase, { recursive: true });
|
|
@@ -617,6 +979,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
617
979
|
cleanupErrors: cleanup.errors,
|
|
618
980
|
decisionReason: cacheDecision.reason,
|
|
619
981
|
},
|
|
982
|
+
model: selectedModel.details,
|
|
620
983
|
turns: 0,
|
|
621
984
|
toolCalls: [],
|
|
622
985
|
startedAt: Date.now(),
|
|
@@ -687,8 +1050,8 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
687
1050
|
modelRegistry: ctx.modelRegistry,
|
|
688
1051
|
resourceLoader,
|
|
689
1052
|
sessionManager: SessionManager.inMemory(workspace),
|
|
690
|
-
model:
|
|
691
|
-
thinkingLevel
|
|
1053
|
+
model: selectedModel.model,
|
|
1054
|
+
thinkingLevel,
|
|
692
1055
|
tools: ["read", "bash"],
|
|
693
1056
|
});
|
|
694
1057
|
|
|
@@ -736,9 +1099,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
736
1099
|
await Promise.race([promptPromise, timeoutPromise]);
|
|
737
1100
|
}
|
|
738
1101
|
|
|
1102
|
+
const lastAssistant = session ? getLastAssistantMessage(session.state.messages) : undefined;
|
|
739
1103
|
const answer = session ? extractLastAssistantText(session.state.messages) : "";
|
|
740
|
-
|
|
741
|
-
|
|
1104
|
+
if (answer) {
|
|
1105
|
+
lastContent = answer;
|
|
1106
|
+
details.status = "done";
|
|
1107
|
+
} else if (aborted) {
|
|
1108
|
+
lastContent = "Aborted";
|
|
1109
|
+
details.status = "aborted";
|
|
1110
|
+
} else {
|
|
1111
|
+
details.status = "error";
|
|
1112
|
+
const reason = describeNoAnswerReason(lastAssistant, details.turns);
|
|
1113
|
+
lastContent = formatInternalFailure(details, reason);
|
|
1114
|
+
details.error = reason;
|
|
1115
|
+
}
|
|
742
1116
|
details.endedAt = Date.now();
|
|
743
1117
|
emit(true);
|
|
744
1118
|
|
|
@@ -807,6 +1181,10 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
807
1181
|
)}${cacheLabel}`;
|
|
808
1182
|
|
|
809
1183
|
const workspaceLine = `${theme.fg("muted", "workspace: ")}${theme.fg("toolOutput", details.workspace)}`;
|
|
1184
|
+
const modelLine = `${theme.fg("muted", "model: ")}${theme.fg("toolOutput", details.model.modelRef)} ${theme.fg(
|
|
1185
|
+
"dim",
|
|
1186
|
+
`(${details.model.thinkingLevel}, ${details.model.autoSelected ? "auto" : "configured"})`,
|
|
1187
|
+
)}`;
|
|
810
1188
|
const cacheLine = `${theme.fg("muted", "cache: ")}${theme.fg("toolOutput", details.cache.root)} ${theme.fg(
|
|
811
1189
|
"dim",
|
|
812
1190
|
`(${details.cache.mode}, ${details.cache.ttlDays}d TTL, cleaned ${details.cache.cleanupDeleted})`,
|
|
@@ -822,7 +1200,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
822
1200
|
if (!expanded && details.toolCalls.length > 6) toolLines.unshift(theme.fg("muted", "…"));
|
|
823
1201
|
|
|
824
1202
|
if (status === "running") {
|
|
825
|
-
const parts = [header, workspaceLine, cacheLine];
|
|
1203
|
+
const parts = [header, workspaceLine, modelLine, cacheLine];
|
|
826
1204
|
if (toolLines.length) parts.push("", theme.fg("muted", "Tools:"), ...toolLines);
|
|
827
1205
|
parts.push("", theme.fg("muted", "Searching GitHub…"));
|
|
828
1206
|
return new Text(parts.join("\n"), 0, 0);
|
|
@@ -830,7 +1208,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
830
1208
|
|
|
831
1209
|
if (!expanded) {
|
|
832
1210
|
const previewLines = answer.split("\n").slice(0, 18);
|
|
833
|
-
const parts = [header, workspaceLine, cacheLine, "", theme.fg("toolOutput", previewLines.join("\n"))];
|
|
1211
|
+
const parts = [header, workspaceLine, modelLine, cacheLine, "", theme.fg("toolOutput", previewLines.join("\n"))];
|
|
834
1212
|
if (answer.split("\n").length > previewLines.length) parts.push(theme.fg("muted", "(Ctrl+O to expand)"));
|
|
835
1213
|
if (toolLines.length) parts.push("", theme.fg("muted", "Tools:"), ...toolLines);
|
|
836
1214
|
return new Text(parts.join("\n"), 0, 0);
|
|
@@ -839,6 +1217,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
839
1217
|
const container = new Container();
|
|
840
1218
|
container.addChild(new Text(header, 0, 0));
|
|
841
1219
|
container.addChild(new Text(workspaceLine, 0, 0));
|
|
1220
|
+
container.addChild(new Text(modelLine, 0, 0));
|
|
842
1221
|
container.addChild(new Text(cacheLine, 0, 0));
|
|
843
1222
|
if (details.cache.cleanupErrors.length) {
|
|
844
1223
|
container.addChild(
|
package/package.json
CHANGED