pi-context-map 0.7.0 → 0.7.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/CHANGELOG.md +13 -0
- package/README.md +19 -14
- package/extensions/analyzer.ts +8 -29
- package/extensions/generator.ts +53 -36
- package/extensions/index.ts +21 -16
- package/extensions/insights.ts +7 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.2] - 2026-06-15
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
- **Fixed insights context window**: Now uses the actual context window from Pi (e.g., 200k) instead of hardcoded 128k. Critical insight no longer shows wrong percentages.
|
|
6
|
+
- **Fixed file filter**: Changed from direct `addEventListener` to event delegation (`document.addEventListener('change', ...)`). Filter now works after SSE body replacement.
|
|
7
|
+
- **Fixed file search**: Changed to event delegation (`document.addEventListener('input', ...)`). Search now works in real-time and survives SSE body replacement.
|
|
8
|
+
- **Added search icon**: SVG magnifying glass icon inside the search input for better UX.
|
|
9
|
+
|
|
10
|
+
## [0.7.1] - 2026-06-15
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
- **Fixed insights dropdown**: Changed from direct `addEventListener` to event delegation (`document.addEventListener('click', ...)` with `e.target.closest('.insight-header')`). The dropdown now works after SSE body replacement.
|
|
13
|
+
- **Fixed auto-open browser**: Added `cmd /c start` with `explorer` fallback for Windows. Browser now opens reliably on first `/context-map` invocation.
|
|
14
|
+
- **Removed unused variable**: Cleaned up `reportPath` destructuring warning in command handler.
|
|
15
|
+
|
|
3
16
|
## [0.7.0] - 2026-06-15
|
|
4
17
|
### Bug Fixes
|
|
5
18
|
- **Fixed token accuracy**: Now uses Pi's actual token count from `ctx.getContextUsage()` instead of heuristic estimation. The usage percentage now matches Pi's terminal display.
|
package/README.md
CHANGED
|
@@ -8,11 +8,15 @@ A visual context window mapping extension for [Pi](https://pi.dev/) that transfo
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
- **Visual Context Budget**: Real-time breakdown of tokens used by System, History, Files, and
|
|
12
|
-
- **
|
|
11
|
+
- **Visual Context Budget**: Real-time breakdown of tokens used by System, Tools, History, Files, and Summaries.
|
|
12
|
+
- **Accurate Token Count**: Uses Pi's actual token count from `ctx.getContextUsage()`, not heuristic estimation — matches the terminal display.
|
|
13
|
+
- **Working Set Analysis**: Categorizes files as `Active`, `Stale`, or `Legacy` based on position in the conversation.
|
|
13
14
|
- **Token Weighting**: Identifies "token hogs" by calculating the approximate size of each file in the window.
|
|
14
|
-
- **Operation Tracking**: Marks files with their last operation (Read
|
|
15
|
-
- **
|
|
15
|
+
- **Operation Tracking**: Marks files with their last operation (Read, Write, Edit).
|
|
16
|
+
- **Compaction Detection**: Tracks compaction summaries and branch summaries as a separate slice.
|
|
17
|
+
- **Auto-Open Browser**: Report automatically opens in your default browser on first invocation.
|
|
18
|
+
- **Dark Mode**: Toggle between light and dark themes. Preference persists across sessions via localStorage.
|
|
19
|
+
- **Live Server**: SSE-powered localhost server with auto-refresh after each assistant message.
|
|
16
20
|
|
|
17
21
|
## Installation
|
|
18
22
|
|
|
@@ -33,20 +37,21 @@ The extension will analyze the session and create an interactive HTML report at:
|
|
|
33
37
|
|
|
34
38
|
## Context Statuses
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
Files are categorized by their position in the conversation (more reliable than turn-based calculation):
|
|
37
41
|
|
|
38
|
-
| Status |
|
|
39
|
-
|
|
40
|
-
| **Active** |
|
|
41
|
-
| **Stale** |
|
|
42
|
-
| **Legacy** |
|
|
42
|
+
| Status | Position in Messages | Action |
|
|
43
|
+
|--------|---------------------|--------|
|
|
44
|
+
| **Active** | Last 30% of messages | Keep in context |
|
|
45
|
+
| **Stale** | Middle 40% of messages | Monitor for removal |
|
|
46
|
+
| **Legacy** | First 30% of messages | Prime candidate for compaction |
|
|
43
47
|
|
|
44
48
|
## How It Works
|
|
45
49
|
|
|
46
|
-
1. **Scanning**: The analyzer iterates through
|
|
47
|
-
2. **Weighting**: It
|
|
48
|
-
3. **
|
|
49
|
-
4. **
|
|
50
|
+
1. **Scanning**: The analyzer iterates through session messages, detecting `toolCall` blocks, `toolResult` messages, `compactionSummary` entries, and image attachments.
|
|
51
|
+
2. **Weighting**: It calculates token counts for each message type using a code-aware heuristic (multipliers for code blocks, strings, etc.).
|
|
52
|
+
3. **Accuracy**: When available, Pi's actual token count from `ctx.getContextUsage()` overrides the heuristic for the usage percentage.
|
|
53
|
+
4. **Categorization**: Files are classified by their position in the message array (last 30% = active, middle 40% = stale, first 30% = legacy).
|
|
54
|
+
5. **Visualization**: Generates a self-contained HTML dashboard with stacked composition bar, file cards with search/filter, dark mode toggle, and interactive insights.
|
|
50
55
|
|
|
51
56
|
## Live Localhost Server
|
|
52
57
|
|
package/extensions/analyzer.ts
CHANGED
|
@@ -148,10 +148,7 @@ export class ContextAnalyzer {
|
|
|
148
148
|
turn,
|
|
149
149
|
timestamp: msg.timestamp || Date.now(),
|
|
150
150
|
},
|
|
151
|
-
status: this.calculateStatus(
|
|
152
|
-
index,
|
|
153
|
-
totalMessages,
|
|
154
|
-
),
|
|
151
|
+
status: this.calculateStatus(index, totalMessages),
|
|
155
152
|
});
|
|
156
153
|
}
|
|
157
154
|
}
|
|
@@ -178,10 +175,7 @@ export class ContextAnalyzer {
|
|
|
178
175
|
turn,
|
|
179
176
|
timestamp: msg.timestamp || Date.now(),
|
|
180
177
|
},
|
|
181
|
-
status: this.calculateStatus(
|
|
182
|
-
index,
|
|
183
|
-
totalMessages,
|
|
184
|
-
),
|
|
178
|
+
status: this.calculateStatus(index, totalMessages),
|
|
185
179
|
});
|
|
186
180
|
}
|
|
187
181
|
}
|
|
@@ -200,10 +194,7 @@ export class ContextAnalyzer {
|
|
|
200
194
|
turn,
|
|
201
195
|
timestamp: msg.timestamp || Date.now(),
|
|
202
196
|
},
|
|
203
|
-
status: this.calculateStatus(
|
|
204
|
-
index,
|
|
205
|
-
totalMessages,
|
|
206
|
-
),
|
|
197
|
+
status: this.calculateStatus(index, totalMessages),
|
|
207
198
|
});
|
|
208
199
|
}
|
|
209
200
|
}
|
|
@@ -224,15 +215,9 @@ export class ContextAnalyzer {
|
|
|
224
215
|
const p = this.extractPath(block.name, block.arguments);
|
|
225
216
|
if (p) {
|
|
226
217
|
const opType = this.getOpType(block.name);
|
|
227
|
-
const result = this.findToolResult(
|
|
228
|
-
messages,
|
|
229
|
-
index,
|
|
230
|
-
block.id,
|
|
231
|
-
);
|
|
218
|
+
const result = this.findToolResult(messages, index, block.id);
|
|
232
219
|
const content = result?.content || "";
|
|
233
|
-
const w = TokenCounter.count(
|
|
234
|
-
String(JSON.stringify(content)),
|
|
235
|
-
);
|
|
220
|
+
const w = TokenCounter.count(String(JSON.stringify(content)));
|
|
236
221
|
fileTokens += w;
|
|
237
222
|
fileRegistry.set(p, {
|
|
238
223
|
path: p,
|
|
@@ -242,10 +227,7 @@ export class ContextAnalyzer {
|
|
|
242
227
|
turn,
|
|
243
228
|
timestamp: msg.timestamp || Date.now(),
|
|
244
229
|
},
|
|
245
|
-
status: this.calculateStatus(
|
|
246
|
-
index,
|
|
247
|
-
totalMessages,
|
|
248
|
-
),
|
|
230
|
+
status: this.calculateStatus(index, totalMessages),
|
|
249
231
|
});
|
|
250
232
|
}
|
|
251
233
|
}
|
|
@@ -263,8 +245,7 @@ export class ContextAnalyzer {
|
|
|
263
245
|
|
|
264
246
|
const mk = (tokens: number): ContextSlice => ({
|
|
265
247
|
tokens: Math.ceil(tokens),
|
|
266
|
-
percent:
|
|
267
|
-
totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
248
|
+
percent: totalTokens > 0 ? Math.round((tokens / totalTokens) * 100) : 0,
|
|
268
249
|
});
|
|
269
250
|
|
|
270
251
|
const files_detail = Array.from(fileRegistry.values())
|
|
@@ -306,9 +287,7 @@ export class ContextAnalyzer {
|
|
|
306
287
|
if (Array.isArray(content)) {
|
|
307
288
|
for (const block of content) {
|
|
308
289
|
if (block.type === "text" && typeof block.text === "string") {
|
|
309
|
-
const match = block.text.match(
|
|
310
|
-
/(?:\/|[A-Z]:\\)[\w./\\-]+\.\w+/,
|
|
311
|
-
);
|
|
290
|
+
const match = block.text.match(/(?:\/|[A-Z]:\\)[\w./\\-]+\.\w+/);
|
|
312
291
|
if (match) return match[0];
|
|
313
292
|
}
|
|
314
293
|
}
|
package/extensions/generator.ts
CHANGED
|
@@ -18,9 +18,10 @@ export class ReportGenerator {
|
|
|
18
18
|
actualTokens?: number | null,
|
|
19
19
|
): string {
|
|
20
20
|
// Use Pi's actual token count when available
|
|
21
|
-
const total =
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const total =
|
|
22
|
+
actualTokens != null && actualTokens > 0
|
|
23
|
+
? actualTokens
|
|
24
|
+
: composition.total.tokens;
|
|
24
25
|
const usagePercent =
|
|
25
26
|
total > 0 ? Math.round((total / contextWindow) * 100) : 0;
|
|
26
27
|
|
|
@@ -311,11 +312,23 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
311
312
|
align-items: center;
|
|
312
313
|
flex-wrap: wrap;
|
|
313
314
|
}
|
|
314
|
-
.file-search {
|
|
315
|
+
.file-search-wrap {
|
|
315
316
|
flex: 1;
|
|
316
317
|
min-width: 220px;
|
|
318
|
+
position: relative;
|
|
319
|
+
}
|
|
320
|
+
.file-search-icon {
|
|
321
|
+
position: absolute;
|
|
322
|
+
left: 16px;
|
|
323
|
+
top: 50%;
|
|
324
|
+
transform: translateY(-50%);
|
|
325
|
+
color: var(--ink-quaternary);
|
|
326
|
+
pointer-events: none;
|
|
327
|
+
}
|
|
328
|
+
.file-search {
|
|
329
|
+
width: 100%;
|
|
317
330
|
height: 44px;
|
|
318
|
-
padding: 0 20px;
|
|
331
|
+
padding: 0 20px 0 40px;
|
|
319
332
|
border: 1px solid rgba(0,0,0,0.08);
|
|
320
333
|
border-radius: 22px;
|
|
321
334
|
background: var(--surface);
|
|
@@ -578,7 +591,10 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
578
591
|
<section>
|
|
579
592
|
<h2>Files <span style="font-size:14px;font-weight:400;color:var(--ink-tertiary);letter-spacing:0;">(${composition.files_detail.length})</span></h2>
|
|
580
593
|
<div class="file-controls">
|
|
581
|
-
<
|
|
594
|
+
<div class="file-search-wrap">
|
|
595
|
+
<svg class="file-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
596
|
+
<input type="text" class="file-search" id="fileSearch" placeholder="Search files..." aria-label="Search files">
|
|
597
|
+
</div>
|
|
582
598
|
<select class="file-filter" id="fileFilter" aria-label="Filter by status">
|
|
583
599
|
<option value="all">All</option>
|
|
584
600
|
<option value="active">Active</option>
|
|
@@ -597,17 +613,19 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
597
613
|
|
|
598
614
|
<script>
|
|
599
615
|
(function() {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
var empty = document.getElementById('emptyState');
|
|
605
|
-
var cards = grid ? Array.from(grid.querySelectorAll('.file-card')) : [];
|
|
606
|
-
var total = cards.length;
|
|
616
|
+
function getCards() {
|
|
617
|
+
var grid = document.getElementById('fileGrid');
|
|
618
|
+
return grid ? Array.from(grid.querySelectorAll('.file-card')) : [];
|
|
619
|
+
}
|
|
607
620
|
|
|
608
621
|
function update() {
|
|
609
|
-
var
|
|
610
|
-
var
|
|
622
|
+
var search = document.getElementById('fileSearch');
|
|
623
|
+
var filter = document.getElementById('fileFilter');
|
|
624
|
+
var count = document.getElementById('fileCount');
|
|
625
|
+
var empty = document.getElementById('emptyState');
|
|
626
|
+
var cards = getCards();
|
|
627
|
+
var q = (search ? search.value : '').toLowerCase();
|
|
628
|
+
var s = filter ? filter.value : 'all';
|
|
611
629
|
var v = 0;
|
|
612
630
|
for (var i = 0; i < cards.length; i++) {
|
|
613
631
|
var c = cards[i];
|
|
@@ -618,11 +636,17 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
618
636
|
if (mq && ms) { c.classList.remove('hidden'); v++; }
|
|
619
637
|
else { c.classList.add('hidden'); }
|
|
620
638
|
}
|
|
621
|
-
count.textContent = v
|
|
622
|
-
empty.style.display = v === 0 ? '' : 'none';
|
|
639
|
+
if (count) count.textContent = v + ' of ' + cards.length;
|
|
640
|
+
if (empty) empty.style.display = v === 0 ? '' : 'none';
|
|
623
641
|
}
|
|
624
|
-
|
|
625
|
-
|
|
642
|
+
|
|
643
|
+
// Event delegation — survives SSE body replacement
|
|
644
|
+
document.addEventListener('input', function(e) {
|
|
645
|
+
if (e.target.id === 'fileSearch') update();
|
|
646
|
+
});
|
|
647
|
+
document.addEventListener('change', function(e) {
|
|
648
|
+
if (e.target.id === 'fileFilter') update();
|
|
649
|
+
});
|
|
626
650
|
update();
|
|
627
651
|
|
|
628
652
|
// Theme toggle
|
|
@@ -647,15 +671,15 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
647
671
|
applyTheme(cur === 'dark' ? 'light' : 'dark');
|
|
648
672
|
});
|
|
649
673
|
|
|
650
|
-
// Insight toggles
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
674
|
+
// Insight toggles (event delegation — survives SSE body replacement)
|
|
675
|
+
document.addEventListener('click', function(e) {
|
|
676
|
+
var btn = e.target.closest('.insight-header');
|
|
677
|
+
if (!btn) return;
|
|
678
|
+
var card = btn.closest('.insight-card');
|
|
679
|
+
if (!card) return;
|
|
680
|
+
var was = card.classList.toggle('collapsed');
|
|
681
|
+
btn.setAttribute('aria-expanded', was ? 'false' : 'true');
|
|
682
|
+
});
|
|
659
683
|
|
|
660
684
|
// Live SSE
|
|
661
685
|
try {
|
|
@@ -679,14 +703,7 @@ h2:first-of-type { margin-top: 48px; }
|
|
|
679
703
|
}
|
|
680
704
|
// Restore theme after body replacement
|
|
681
705
|
applyTheme(currentTheme);
|
|
682
|
-
//
|
|
683
|
-
var ns = document.getElementById('fileSearch');
|
|
684
|
-
var nf = document.getElementById('fileFilter');
|
|
685
|
-
if (ns) ns.addEventListener('input', update);
|
|
686
|
-
if (nf) nf.addEventListener('change', update);
|
|
687
|
-
// Re-query cards after body replacement
|
|
688
|
-
cards = Array.from(document.getElementById('fileGrid').querySelectorAll('.file-card'));
|
|
689
|
-
total = cards.length;
|
|
706
|
+
// Search/filter use event delegation — no re-binding needed
|
|
690
707
|
update();
|
|
691
708
|
}
|
|
692
709
|
} catch(_) {}
|
package/extensions/index.ts
CHANGED
|
@@ -27,21 +27,29 @@ function makeReportPath(sessionName?: string): string {
|
|
|
27
27
|
const now = new Date();
|
|
28
28
|
const date = now.toISOString().split("T")[0];
|
|
29
29
|
const time = now.toTimeString().split(" ")[0].replace(/:/g, "-");
|
|
30
|
-
const safe = (sessionName || "session")
|
|
31
|
-
.replace(/[^\w.-]/g, "_")
|
|
32
|
-
.slice(0, 40);
|
|
30
|
+
const safe = (sessionName || "session").replace(/[^\w.-]/g, "_").slice(0, 40);
|
|
33
31
|
const filename = `${date}_${time}_${safe}.html`;
|
|
34
32
|
return path.join(dir, filename);
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
function openBrowser(url: string): void {
|
|
38
36
|
const platform = process.platform;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
try {
|
|
38
|
+
if (platform === "win32") {
|
|
39
|
+
// Use cmd /c start with separate title arg to handle URLs with special chars
|
|
40
|
+
exec(`cmd /c start "" "${url}"`, (err) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
// Fallback: try explorer directly
|
|
43
|
+
exec(`explorer "${url}"`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
} else if (platform === "darwin") {
|
|
47
|
+
exec(`open "${url}"`);
|
|
48
|
+
} else {
|
|
49
|
+
exec(`xdg-open "${url}"`);
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Silent — browser open is best-effort
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -155,7 +163,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
155
163
|
}
|
|
156
164
|
}
|
|
157
165
|
|
|
158
|
-
const insights = InsightEngine.generate(composition);
|
|
166
|
+
const insights = InsightEngine.generate(composition, contextWindow);
|
|
159
167
|
const html = ReportGenerator.generateHTML(
|
|
160
168
|
composition,
|
|
161
169
|
insights,
|
|
@@ -194,7 +202,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
194
202
|
|
|
195
203
|
ctx.ui.notify("Analyzing session context...", "info");
|
|
196
204
|
try {
|
|
197
|
-
const { composition, insights
|
|
205
|
+
const { composition, insights } = await runAnalysis();
|
|
198
206
|
const criticalCount = insights.filter(
|
|
199
207
|
(i) => i.severity === "critical",
|
|
200
208
|
).length;
|
|
@@ -253,9 +261,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
253
261
|
actualPercent != null
|
|
254
262
|
? actualPercent
|
|
255
263
|
: composition.total.tokens > 0
|
|
256
|
-
? Math.round(
|
|
257
|
-
(composition.total.tokens / contextWindow) * 100,
|
|
258
|
-
)
|
|
264
|
+
? Math.round((composition.total.tokens / contextWindow) * 100)
|
|
259
265
|
: 0;
|
|
260
266
|
const summary =
|
|
261
267
|
`Context: ${composition.total.tokens.toLocaleString()} tokens (${usagePercent.toFixed(1)}% of ${(contextWindow / 1000).toFixed(0)}k). ` +
|
|
@@ -270,8 +276,7 @@ export default async function piContextMap(pi: ExtensionAPI): Promise<void> {
|
|
|
270
276
|
summary,
|
|
271
277
|
"",
|
|
272
278
|
...insights.map(
|
|
273
|
-
(i) =>
|
|
274
|
-
`[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
|
|
279
|
+
(i) => `[${i.severity.toUpperCase()}] ${i.title}: ${i.message}`,
|
|
275
280
|
),
|
|
276
281
|
`Report: ${reportPath}`,
|
|
277
282
|
serverUrl ? `Live: ${serverUrl}` : "",
|
package/extensions/insights.ts
CHANGED
|
@@ -14,15 +14,17 @@ export interface Insight {
|
|
|
14
14
|
command?: string; // Suggested slash command
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const TYPICAL_WINDOW = 128_000; // Default context window size (Claude Sonnet/Opus)
|
|
18
|
-
|
|
19
17
|
export class InsightEngine {
|
|
20
18
|
/**
|
|
21
19
|
* Generate a list of insights based on the composition.
|
|
22
20
|
*/
|
|
23
|
-
public static generate(
|
|
21
|
+
public static generate(
|
|
22
|
+
composition: ContextComposition,
|
|
23
|
+
contextWindow?: number,
|
|
24
|
+
): Insight[] {
|
|
24
25
|
const insights: Insight[] = [];
|
|
25
|
-
const { system, tools,
|
|
26
|
+
const { system, tools, files, summaries, total } = composition;
|
|
27
|
+
const windowSize = contextWindow || 128_000;
|
|
26
28
|
|
|
27
29
|
// Rule 1: Tool bloat
|
|
28
30
|
if (tools.percent > 40) {
|
|
@@ -50,7 +52,7 @@ export class InsightEngine {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Rule 3: High overall usage
|
|
53
|
-
const usagePercent = Math.round((total.tokens /
|
|
55
|
+
const usagePercent = Math.round((total.tokens / windowSize) * 100);
|
|
54
56
|
if (usagePercent > 80) {
|
|
55
57
|
insights.push({
|
|
56
58
|
id: "high-usage",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-map",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Professional context profiler for Pi that visualizes the session context window, token distribution, and integrates with Nexus packages for actionable insights.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|