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 +10 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/config/config.example.json +4 -0
- package/index.ts +3 -0
- package/package.json +66 -0
- package/src/config-store.ts +81 -0
- package/src/constants.ts +3 -0
- package/src/index.ts +27 -0
- package/src/session-agent.ts +78 -0
- package/src/session-cleanup-command.ts +268 -0
- package/src/session-delete.ts +50 -0
- package/src/session-format.ts +98 -0
- package/src/session-nix-command.ts +84 -0
- package/src/session-selection.ts +167 -0
- package/src/session-sort.ts +137 -0
- package/src/session-source.ts +32 -0
- package/src/tui/session-cleanup-picker.ts +592 -0
- package/src/types-shims.d.ts +192 -0
- package/src/types.ts +39 -0
- package/src/ui/icons.ts +476 -0
- package/src/ui/legend.ts +15 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { SessionInfo } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
function toTimestamp(value: unknown): number | null {
|
|
6
|
+
if (value instanceof Date) {
|
|
7
|
+
const timestamp = value.getTime();
|
|
8
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof value === "number") {
|
|
12
|
+
return Number.isFinite(value) ? value : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parsed = Date.parse(value);
|
|
20
|
+
if (!Number.isFinite(parsed)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseEpochCandidate(token: string): number | null {
|
|
28
|
+
if (!/^\d{10,17}$/.test(token)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const numeric = Number(token);
|
|
33
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (token.length <= 10) {
|
|
38
|
+
return numeric * 1000;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return numeric;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseCalendarCandidate(path: string): number | null {
|
|
45
|
+
const match = path.match(
|
|
46
|
+
/(\d{4})[-_]?([01]\d)[-_]?([0-3]\d)(?:[tT _-]?([0-2]\d)[:_\-]?([0-5]\d)?[:_\-]?([0-5]\d)?)?/,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!match) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const year = Number(match[1]);
|
|
54
|
+
const month = Number(match[2]);
|
|
55
|
+
const day = Number(match[3]);
|
|
56
|
+
const hour = Number(match[4] ?? "0");
|
|
57
|
+
const minute = Number(match[5] ?? "0");
|
|
58
|
+
const second = Number(match[6] ?? "0");
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
!Number.isFinite(year) ||
|
|
62
|
+
!Number.isFinite(month) ||
|
|
63
|
+
!Number.isFinite(day) ||
|
|
64
|
+
!Number.isFinite(hour) ||
|
|
65
|
+
!Number.isFinite(minute) ||
|
|
66
|
+
!Number.isFinite(second)
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const utcTimestamp = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
72
|
+
return Number.isFinite(utcTimestamp) ? utcTimestamp : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function timestampFromPath(sessionPath: string): number {
|
|
76
|
+
const fileName = basename(sessionPath);
|
|
77
|
+
const epochCandidates = fileName.match(/\d{10,17}/g) ?? [];
|
|
78
|
+
|
|
79
|
+
let bestEpoch = 0;
|
|
80
|
+
for (const candidate of epochCandidates) {
|
|
81
|
+
const parsed = parseEpochCandidate(candidate);
|
|
82
|
+
if (parsed && parsed > bestEpoch) {
|
|
83
|
+
bestEpoch = parsed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (bestEpoch > 0) {
|
|
88
|
+
return bestEpoch;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const calendarTimestamp = parseCalendarCandidate(fileName);
|
|
92
|
+
if (calendarTimestamp) {
|
|
93
|
+
return calendarTimestamp;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compareDescending(left: number, right: number): number {
|
|
100
|
+
if (left === right) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return right - left;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function sortSessionsNewestFirst(
|
|
108
|
+
sessions: readonly SessionInfo[],
|
|
109
|
+
): SessionInfo[] {
|
|
110
|
+
return [...sessions].sort((left, right) => {
|
|
111
|
+
const modifiedOrder = compareDescending(
|
|
112
|
+
toTimestamp(left.modified) ?? 0,
|
|
113
|
+
toTimestamp(right.modified) ?? 0,
|
|
114
|
+
);
|
|
115
|
+
if (modifiedOrder !== 0) {
|
|
116
|
+
return modifiedOrder;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const createdOrder = compareDescending(
|
|
120
|
+
toTimestamp(left.created) ?? 0,
|
|
121
|
+
toTimestamp(right.created) ?? 0,
|
|
122
|
+
);
|
|
123
|
+
if (createdOrder !== 0) {
|
|
124
|
+
return createdOrder;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pathOrder = compareDescending(
|
|
128
|
+
timestampFromPath(left.path),
|
|
129
|
+
timestampFromPath(right.path),
|
|
130
|
+
);
|
|
131
|
+
if (pathOrder !== 0) {
|
|
132
|
+
return pathOrder;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return right.path.localeCompare(left.path);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SessionManager,
|
|
3
|
+
type ExtensionCommandContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import { enrichSessionWithResponsibleAgent } from "./session-agent.js";
|
|
7
|
+
import { sortSessionsNewestFirst } from "./session-sort.js";
|
|
8
|
+
import type { SessionCleanupSession, SessionScope } from "./types.js";
|
|
9
|
+
|
|
10
|
+
function ensureSessionArray(value: unknown): SessionCleanupSession[] {
|
|
11
|
+
if (!Array.isArray(value)) {
|
|
12
|
+
throw new Error("Session manager returned a non-array response.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return value as SessionCleanupSession[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadSessions(
|
|
19
|
+
ctx: ExtensionCommandContext,
|
|
20
|
+
scope: SessionScope,
|
|
21
|
+
): Promise<SessionCleanupSession[]> {
|
|
22
|
+
const loaded =
|
|
23
|
+
scope === "all"
|
|
24
|
+
? await SessionManager.listAll()
|
|
25
|
+
: await SessionManager.list(
|
|
26
|
+
ctx.sessionManager.getCwd(),
|
|
27
|
+
ctx.sessionManager.getSessionDir(),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const sortedSessions = sortSessionsNewestFirst(ensureSessionArray(loaded));
|
|
31
|
+
return Promise.all(sortedSessions.map((session) => enrichSessionWithResponsibleAgent(session)));
|
|
32
|
+
}
|