pi-session-cleanup 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 - 2026-03-05
4
+
5
+ - Renamed extension from its previous package name to `pi-session-cleanup`
6
+ - Renamed primary command to `/session-cleanup`
7
+ - Added a deprecated legacy alias for backward compatibility
8
+ - Refactored command/picker module names for clarity
9
+ - Upgraded custom picker UI with bordered modal layout, title/status/help lines, and responsive overlay sizing
10
+ - Added production-ready package metadata and README
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MasuRii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # pi-session-cleanup
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pi-session-cleanup?style=flat-square)](https://www.npmjs.com/package/pi-session-cleanup) [![License](https://img.shields.io/github/license/MasuRii/pi-session-cleanup?style=flat-square)](LICENSE)
4
+
5
+ Interactive session cleanup extension for the [Pi coding agent](https://github.com/mariozechner/pi).
6
+
7
+ <img width="1389" height="768" alt="image" src="https://github.com/user-attachments/assets/42464ca1-4a6c-4496-b13f-5bcc2093bf59" />
8
+
9
+ **pi-session-cleanup** provides a focused TUI command for batch-selecting historical sessions and deleting them safely with trash-first fallback and active session protection.
10
+
11
+ ## Features
12
+
13
+ - **Interactive Session Cleanup** — Browse, select, and delete sessions via an intuitive modal interface
14
+ - **Scope Filtering** — View only orphaned sessions or all historical sessions
15
+ - **Batch Selection Controls** — Multi-select with Space, select all with `a`, keyboard navigation
16
+ - **Safe Delete Flow** — Excludes the currently active session and uses trash-first deletion with unlink fallback
17
+ - **Improved Modal UX** — Centered overlay with bordered layout, concise single-line legend, status summary, and automatic icon fallback
18
+
19
+ ## Installation
20
+
21
+ ### Local Extension Folder
22
+
23
+ Place this folder in one of Pi's auto-discovery paths:
24
+
25
+ ```text
26
+ ~/.pi/agent/extensions/pi-session-cleanup # Global default (when PI_CODING_AGENT_DIR is unset)
27
+ .pi/extensions/pi-session-cleanup # Project-specific
28
+ ```
29
+
30
+ Pi will auto-discover the extension on startup.
31
+
32
+ ### As NPM Package
33
+
34
+ ```bash
35
+ pi install npm:pi-session-cleanup
36
+ ```
37
+
38
+ ### Git Repository
39
+
40
+ ```bash
41
+ pi install git:github.com/MasuRii/pi-session-cleanup
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Commands
47
+
48
+ | Command | Arguments | Description |
49
+ |---------|-----------|-------------|
50
+ | `/session-cleanup` | — | Opens the session cleanup modal showing orphaned sessions only |
51
+ | `/session-cleanup current` | — | Opens modal with sessions from the current directory |
52
+ | `/session-cleanup all` | — | Opens modal showing all sessions |
53
+ | `/session-cleanup help` | — | Displays usage help |
54
+
55
+ **Scopes:**
56
+
57
+ - **Default (no args)** — Shows orphaned sessions (sessions without a matching directory)
58
+ - **`current`** — Shows sessions from the current working directory
59
+ - **`all`** — Shows all historical sessions across all directories
60
+
61
+ ### Modal Controls
62
+
63
+ When the session picker modal is open:
64
+
65
+ | Key | Action |
66
+ |-----|--------|
67
+ | `↑` / `↓` / `j` / `k` | Navigate up/down in the list |
68
+ | `PgUp` / `PgDn` | Page up/down through sessions |
69
+ | `Home` / `End` | Jump to first/last item |
70
+ | `Space` | Toggle selection of current item |
71
+ | `a` | Select all visible sessions |
72
+ | `r` | Refresh the session list |
73
+ | `Enter` | Confirm deletion of selected sessions |
74
+ | `Esc` / `q` / `Ctrl+C` | Cancel and close modal |
75
+
76
+ ### Safety Guards
77
+
78
+ The extension includes multiple safety mechanisms:
79
+
80
+ 1. **Active Session Protection** — The currently active session is never shown in the list and cannot be deleted
81
+ 2. **Trash-First Deletion** — Sessions are moved to trash first; only falls back to permanent deletion if trash is unavailable
82
+ 3. **Confirmation Required** — The modal requires explicit `Enter` keypress to proceed with deletion
83
+ 4. **Escapable** — `Esc` or `q` immediately cancels without any changes
84
+
85
+ ## Configuration
86
+
87
+ Configuration is stored at:
88
+
89
+ ```text
90
+ Default global path: ~/.pi/agent/extensions/pi-session-cleanup/config.json
91
+ Actual global path: $PI_CODING_AGENT_DIR/extensions/pi-session-cleanup/config.json when PI_CODING_AGENT_DIR is set
92
+ ```
93
+
94
+ A starter template is provided in `config/config.example.json`. On startup, the extension creates `config.json` with defaults if missing.
95
+
96
+ ### Configuration Options
97
+
98
+ | Option | Type | Default | Description |
99
+ |--------|------|---------|-------------|
100
+ | `enabled` | `boolean` | `true` | Master on/off switch for the extension |
101
+ | `iconMode` | `"auto" \| "nerd" \| "fallback"` | `"auto"` | Icon rendering mode for the modal UI (`auto` detects Nerd Font usage in supported terminals and safely falls back otherwise) |
102
+
103
+ ### Icon Mode Overrides
104
+
105
+ You can override icon mode without editing config:
106
+
107
+ - `PI_SESSION_CLEANUP_ICON_MODE=nerd|fallback|auto`
108
+ - `PI_SESSION_CLEANUP_NERD_FONT=true|false` (or `PI_NERD_FONT=true|false`)
109
+
110
+ `auto` now prefers Nerd icons when Nerd Font is actually configured (including Windows Terminal profile/default font checks) and falls back to safe icons when detection is unavailable or uncertain.
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ npm run build # Type-check with TypeScript
116
+ npm run lint # Run linting (same as build)
117
+ npm run test # Run test suite
118
+ npm run check # Run full verification (build + test)
119
+ npm run package:dry-run
120
+ ```
121
+
122
+ ## Publishing
123
+
124
+ The package metadata follows the same publish-ready shape used by established Pi extensions:
125
+
126
+ - entrypoint: `index.ts`
127
+ - package exports: `.` → `./index.ts`
128
+ - Pi extension manifest: `pi.extensions`
129
+ - published files: source, README, changelog, license, and config template
130
+ - runtime `config.json`, tests, and build artifacts excluded from npm publication
131
+
132
+ ## Related Pi Extensions
133
+
134
+ - [pi-hide-messages](https://github.com/MasuRii/pi-hide-messages) — Hide older TUI chat history while preserving full session context
135
+ - [pi-context-injector](https://github.com/MasuRii/pi-context-injector) — Inject compact project context into first-turn and compaction prompts
136
+ - [pi-tool-display](https://github.com/MasuRii/pi-tool-display) — Compact tool rendering and diff visualization
137
+ - [pi-rtk-optimizer](https://github.com/MasuRii/pi-rtk-optimizer) — RTK command rewriting and output compaction
138
+
139
+ ## Changelog
140
+
141
+ See [CHANGELOG.md](CHANGELOG.md) for version history.
142
+
143
+ ## License
144
+
145
+ [MIT](LICENSE) © MasuRii
@@ -0,0 +1,4 @@
1
+ {
2
+ "iconMode": "auto",
3
+ "enabled": true
4
+ }
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import sessionCleanupExtension from "./src/index.js";
2
+
3
+ export default sessionCleanupExtension;
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "pi-session-cleanup",
3
+ "version": "1.0.0",
4
+ "description": "Pi extension for interactive batch session cleanup and safe deletion.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "config/config.example.json",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
20
+ "lint": "npm run build",
21
+ "test:clean": "node -e \"require('node:fs').rmSync('.test-dist', { recursive: true, force: true })\"",
22
+ "pretest": "npm run test:clean",
23
+ "test": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.test.json && node --test .test-dist/test/*.test.js",
24
+ "posttest": "npm run test:clean",
25
+ "check": "npm run lint && npm run test",
26
+ "package:dry-run": "npm pack --dry-run"
27
+ },
28
+ "keywords": [
29
+ "pi-package",
30
+ "pi",
31
+ "pi-extension",
32
+ "session",
33
+ "cleanup",
34
+ "delete",
35
+ "pi-coding-agent",
36
+ "pi-tui",
37
+ "session-management",
38
+ "tui",
39
+ "safe-delete"
40
+ ],
41
+ "author": "MasuRii",
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "pi": {
50
+ "extensions": [
51
+ "./index.ts"
52
+ ]
53
+ },
54
+ "peerDependencies": {
55
+ "@mariozechner/pi-coding-agent": "^0.70.5",
56
+ "@mariozechner/pi-tui": "^0.70.5"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/MasuRii/pi-session-cleanup.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/MasuRii/pi-session-cleanup/issues"
64
+ },
65
+ "homepage": "https://github.com/MasuRii/pi-session-cleanup#readme"
66
+ }
@@ -0,0 +1,81 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export type IconModePreference = "auto" | "nerd" | "fallback";
6
+
7
+ export interface SessionCleanupConfig {
8
+ enabled: boolean;
9
+ iconMode: IconModePreference;
10
+ }
11
+
12
+ const DEFAULT_CONFIG: SessionCleanupConfig = {
13
+ enabled: true,
14
+ iconMode: "auto",
15
+ };
16
+
17
+ function resolveExtensionRoot(): string {
18
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
19
+ }
20
+
21
+ function parseIconMode(value: unknown): IconModePreference | null {
22
+ if (typeof value !== "string") {
23
+ return null;
24
+ }
25
+
26
+ const normalized = value.trim().toLowerCase();
27
+ if (normalized === "auto" || normalized === "nerd" || normalized === "fallback") {
28
+ return normalized;
29
+ }
30
+
31
+ return null;
32
+ }
33
+
34
+ function readConfigFile(configPath: string): Record<string, unknown> | null {
35
+ if (!existsSync(configPath)) {
36
+ return null;
37
+ }
38
+
39
+ try {
40
+ const raw = readFileSync(configPath, "utf8");
41
+ const parsed = JSON.parse(raw);
42
+ if (parsed && typeof parsed === "object") {
43
+ return parsed as Record<string, unknown>;
44
+ }
45
+ } catch {
46
+ // Fall back to defaults when config parsing fails.
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ function ensureConfigFile(configPath: string): void {
53
+ if (existsSync(configPath)) {
54
+ return;
55
+ }
56
+
57
+ try {
58
+ mkdirSync(dirname(configPath), { recursive: true });
59
+ writeFileSync(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, "utf8");
60
+ } catch {
61
+ // Ignore file system errors and continue with in-memory defaults.
62
+ }
63
+ }
64
+
65
+ export function loadSessionCleanupConfig(): SessionCleanupConfig {
66
+ const configPath = join(resolveExtensionRoot(), "config.json");
67
+ ensureConfigFile(configPath);
68
+
69
+ const raw = readConfigFile(configPath);
70
+ if (!raw) {
71
+ return { ...DEFAULT_CONFIG };
72
+ }
73
+
74
+ const enabled = typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_CONFIG.enabled;
75
+ const iconMode = parseIconMode(raw.iconMode) ?? DEFAULT_CONFIG.iconMode;
76
+
77
+ return {
78
+ enabled,
79
+ iconMode,
80
+ };
81
+ }
@@ -0,0 +1,3 @@
1
+ export const EXTENSION_ID = "pi-session-cleanup";
2
+ export const SESSION_CLEANUP_COMMAND = "session-cleanup";
3
+ export const SESSION_NIX_COMMAND = "nix";
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ import { SESSION_CLEANUP_COMMAND, SESSION_NIX_COMMAND } from "./constants.js";
4
+ import {
5
+ getSessionCleanupArgumentCompletions,
6
+ handleSessionCleanupCommand,
7
+ } from "./session-cleanup-command.js";
8
+ import { handleSessionNixCommand } from "./session-nix-command.js";
9
+
10
+ export default function sessionCleanupExtension(pi: ExtensionAPI): void {
11
+ pi.registerCommand(SESSION_CLEANUP_COMMAND, {
12
+ description:
13
+ "Batch-select previous sessions and delete them with confirmation.",
14
+ getArgumentCompletions: getSessionCleanupArgumentCompletions,
15
+ handler: async (args, ctx) => {
16
+ await handleSessionCleanupCommand(args, ctx);
17
+ },
18
+ });
19
+
20
+ pi.registerCommand(SESSION_NIX_COMMAND, {
21
+ description:
22
+ "Start a new session and automatically delete the previous session.",
23
+ handler: async (args, ctx) => {
24
+ await handleSessionNixCommand(args, ctx);
25
+ },
26
+ });
27
+ }
@@ -0,0 +1,78 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { SessionInfo } from "@mariozechner/pi-coding-agent";
4
+
5
+ import type { SessionCleanupSession } from "./types.js";
6
+
7
+ function toRecord(value: unknown): Record<string, unknown> | null {
8
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
9
+ }
10
+
11
+ function normalizeAgentName(value: unknown): string | null {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+
16
+ const trimmed = value.trim();
17
+ return trimmed.length > 0 ? trimmed : null;
18
+ }
19
+
20
+ function parseJsonLine(value: string): Record<string, unknown> | null {
21
+ try {
22
+ return toRecord(JSON.parse(value));
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ export function extractResponsibleAgentNameFromContent(content: string): string | null {
29
+ const lines = content.split(/\r?\n/);
30
+
31
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
32
+ const trimmed = lines[index]?.trim();
33
+ if (!trimmed) {
34
+ continue;
35
+ }
36
+
37
+ const entry = parseJsonLine(trimmed);
38
+ if (!entry || entry.type !== "custom" || entry.customType !== "active_agent") {
39
+ continue;
40
+ }
41
+
42
+ const data = toRecord(entry.data);
43
+ const normalizedAgentName = normalizeAgentName(data?.name);
44
+ if (normalizedAgentName) {
45
+ return normalizedAgentName;
46
+ }
47
+
48
+ if (data?.name === null) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ export async function resolveResponsibleAgentName(sessionPath: string): Promise<string | null> {
57
+ if (typeof sessionPath !== "string" || sessionPath.trim().length === 0) {
58
+ return null;
59
+ }
60
+
61
+ try {
62
+ const content = await readFile(sessionPath, "utf8");
63
+ return extractResponsibleAgentNameFromContent(content);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ export async function enrichSessionWithResponsibleAgent(
70
+ session: SessionInfo,
71
+ ): Promise<SessionCleanupSession> {
72
+ const responsibleAgentName = await resolveResponsibleAgentName(session.path);
73
+
74
+ return {
75
+ ...session,
76
+ responsibleAgentName,
77
+ };
78
+ }