agent-session-kill 0.1.0 → 0.2.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/README.md +46 -9
- package/package.json +4 -2
- package/src/cli.js +8 -3
- package/src/protection.js +13 -0
- package/src/scanner.js +110 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
NPKILL-style cleanup for AI coding-agent session remnants.
|
|
4
4
|
|
|
5
|
-
`agent-session-kill` scans local Claude Code, Pi/pi-mono, Oh My Pi (OMP), and subagent temp storage, then lets you review and delete stale session artifacts from an interactive terminal UI.
|
|
5
|
+
`agent-session-kill` scans local Claude Code, Pi/pi-mono, Oh My Pi (OMP), GitHub Copilot Chat, and subagent temp storage, then lets you review and delete stale session artifacts from an interactive terminal UI.
|
|
6
6
|
|
|
7
7
|
## Why
|
|
8
8
|
|
|
@@ -32,13 +32,36 @@ npx agent-session-kill
|
|
|
32
32
|
## Commands
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
agent-session-kill
|
|
41
|
-
agent-session-kill
|
|
35
|
+
# Interactive picker (default)
|
|
36
|
+
agent-session-kill
|
|
37
|
+
agentkill # short alias
|
|
38
|
+
|
|
39
|
+
# Limit to specific tools
|
|
40
|
+
agent-session-kill --tool copilot # Copilot Chat only
|
|
41
|
+
agent-session-kill --tool claude,omp # multiple tools
|
|
42
|
+
|
|
43
|
+
# Orphan detection — OMP sessions for deleted projects trashed regardless of age
|
|
44
|
+
agent-session-kill --workspace ~/workspace
|
|
45
|
+
|
|
46
|
+
# Include cache files as cleanup candidates
|
|
47
|
+
agent-session-kill --include-cache
|
|
48
|
+
|
|
49
|
+
# Non-interactive inventory
|
|
50
|
+
agent-session-kill scan # table output
|
|
51
|
+
agent-session-kill scan --json # machine-readable
|
|
52
|
+
agent-session-kill scan --tool copilot --json # scoped to one tool
|
|
53
|
+
agent-session-kill scan --older-than 7d # tighter age threshold
|
|
54
|
+
|
|
55
|
+
# Dry-run manifest (print what would be deleted)
|
|
56
|
+
agent-session-kill clean --dry-run
|
|
57
|
+
agent-session-kill clean --dry-run --include-cache # include cache in preview
|
|
58
|
+
|
|
59
|
+
# Apply cleanup
|
|
60
|
+
agent-session-kill clean --apply # trash (recoverable)
|
|
61
|
+
agent-session-kill clean --apply --permanent # permanent delete
|
|
62
|
+
|
|
63
|
+
# Delegate cleanup to native tools (claude project purge, omp worktree clear)
|
|
64
|
+
agent-session-kill clean --dry-run --delegates
|
|
42
65
|
```
|
|
43
66
|
|
|
44
67
|
## Interactive controls
|
|
@@ -63,12 +86,13 @@ Rows that are protected or kept are visible but not selectable.
|
|
|
63
86
|
| Option | Description | Default |
|
|
64
87
|
| --- | --- | --- |
|
|
65
88
|
| `--older-than <duration>` | Minimum age for stale artifacts. Supports `m`, `h`, `d`. | `14d` |
|
|
66
|
-
| `--tool <name>` | Limit to `claude`, `pi`, `omp`, or `
|
|
89
|
+
| `--tool <name>` | Limit to `claude`, `pi`, `omp`, `temp`, or `copilot`. Repeat or comma-separate. | all |
|
|
67
90
|
| `--include-cache` | Include cache roots as cleanup candidates. | off |
|
|
68
91
|
| `--json` | Print JSON. Always non-interactive. | off |
|
|
69
92
|
| `--delegates` | Run delegated dry-runs (`claude project purge`, `omp worktree clear`). | off |
|
|
70
93
|
| `--home <path>` | Home directory to scan. | current user home |
|
|
71
94
|
| `--temp <path>` | Temp directory to scan. | OS temp dir |
|
|
95
|
+
| `--workspace <path>` | Workspace directory; OMP sessions for projects no longer on disk are flagged as orphaned and trashed regardless of age (scans 2 levels deep). | off |
|
|
72
96
|
| `--dry-run` | Force dry-run mode. | off |
|
|
73
97
|
| `--apply` | Apply cleanup in `clean` mode. | off |
|
|
74
98
|
| `--permanent` | Permanently delete instead of trashing. | off |
|
|
@@ -106,6 +130,16 @@ Claude project transcripts under `~/.claude/projects/` are protected from direct
|
|
|
106
130
|
|
|
107
131
|
OMP worktrees are handled through `omp worktree clear --dry-run` when delegates are enabled.
|
|
108
132
|
|
|
133
|
+
### GitHub Copilot Chat (VS Code)
|
|
134
|
+
|
|
135
|
+
- `ask-agent/`, `plan-agent/`, `explore-agent/`, `logContextRecordings/`, `memory-tool/`
|
|
136
|
+
- `copilot.cli.oldGlobalSessions.json`
|
|
137
|
+
- `copilot.cli.workspaceSessions.*.json`
|
|
138
|
+
- `commandEmbeddings.json`, `settingEmbeddings.json`, `toolEmbeddingsCache.bin`, `copilot-cli-images/` only when `--include-cache` is set
|
|
139
|
+
|
|
140
|
+
macOS path: `~/Library/Application Support/Code/User/globalStorage/github.copilot-chat`
|
|
141
|
+
Linux path: `~/.config/Code/User/globalStorage/github.copilot-chat`
|
|
142
|
+
|
|
109
143
|
### Subagent temp runs
|
|
110
144
|
|
|
111
145
|
- `<temp>/pi-subagents-*/chain-runs/`
|
|
@@ -132,6 +166,9 @@ Examples:
|
|
|
132
166
|
- `~/.omp/agent/config.yml`
|
|
133
167
|
- `~/.omp/agent/managed-skills/`
|
|
134
168
|
- `~/.omp/agent/memories/`
|
|
169
|
+
- `~/.../github.copilot-chat/api.json`
|
|
170
|
+
- `~/.../github.copilot-chat/mcpServers.json`
|
|
171
|
+
- `~/.../github.copilot-chat/debugCommand`
|
|
135
172
|
|
|
136
173
|
## Safety model
|
|
137
174
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-session-kill",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "NPKILL-style cleanup for Claude, Pi, and
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "NPKILL-style cleanup for Claude, Pi, OMP, and GitHub Copilot Chat agent session remnants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Kyle Brodeur",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"claude-code",
|
|
18
|
+
"github-copilot",
|
|
19
|
+
"copilot",
|
|
18
20
|
"oh-my-pi",
|
|
19
21
|
"pi",
|
|
20
22
|
"sessions",
|
package/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { formatJson, formatTable } from "./format.js";
|
|
|
9
9
|
import { scanRemnants } from "./scanner.js";
|
|
10
10
|
import { applyManifest } from "./apply.js";
|
|
11
11
|
import { runInteractive } from "./interactive.js";
|
|
12
|
-
const TOOLS = new Set(["claude", "pi", "omp", "temp"]);
|
|
12
|
+
const TOOLS = new Set(["claude", "pi", "omp", "temp", "copilot"]);
|
|
13
13
|
|
|
14
14
|
function usage() {
|
|
15
15
|
return buildProgram().helpInformation().trimEnd();
|
|
@@ -38,6 +38,7 @@ function buildDefaultOptions() {
|
|
|
38
38
|
tempDir: os.tmpdir(),
|
|
39
39
|
dryRun: false,
|
|
40
40
|
tools: new Set(TOOLS),
|
|
41
|
+
workspaceDir: undefined,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -53,16 +54,17 @@ export function buildProgram() {
|
|
|
53
54
|
|
|
54
55
|
program
|
|
55
56
|
.name("agent-session-kill")
|
|
56
|
-
.description("NPKILL-style cleanup for Claude, Pi, and
|
|
57
|
+
.description("NPKILL-style cleanup for Claude, Pi, OMP, and Copilot agent session remnants")
|
|
57
58
|
.exitOverride()
|
|
58
59
|
.allowExcessArguments(false)
|
|
59
60
|
.option("--older-than <duration>", "minimum age to consider stale", "14d")
|
|
60
|
-
.option("--tool <name>", "limit to claude, pi, omp, or
|
|
61
|
+
.option("--tool <name>", "limit to claude, pi, omp, temp, or copilot; repeat or comma-separate", collectTools, [])
|
|
61
62
|
.option("--include-cache", "include cache roots in cleanup candidates", false)
|
|
62
63
|
.option("--json", "print JSON instead of a table", false)
|
|
63
64
|
.option("--delegates", "run delegate dry-run cleanup commands", false)
|
|
64
65
|
.option("--home <path>", "home directory to scan", os.homedir())
|
|
65
66
|
.option("--temp <path>", "temp directory to scan", os.tmpdir())
|
|
67
|
+
.option("--workspace <path>", "workspace directory for orphan detection")
|
|
66
68
|
.option("--dry-run", "force dry-run mode", false)
|
|
67
69
|
.option("--permanent", "permanently delete instead of trash", false);
|
|
68
70
|
|
|
@@ -112,6 +114,7 @@ export function parseCliArgs(argv) {
|
|
|
112
114
|
tempDir: parsed.temp,
|
|
113
115
|
dryRun: parsed.dryRun,
|
|
114
116
|
tools: parsed.tool.length > 0 ? new Set(parsed.tool) : new Set(TOOLS),
|
|
117
|
+
workspaceDir: parsed.workspace,
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
if (options.dryRun) {
|
|
@@ -156,6 +159,7 @@ export async function main(argv = process.argv.slice(2), io = { stdin: process.s
|
|
|
156
159
|
apply: false,
|
|
157
160
|
permanent: options.permanent,
|
|
158
161
|
tools: options.tools,
|
|
162
|
+
workspaceDir: options.workspaceDir,
|
|
159
163
|
}, {
|
|
160
164
|
stdin,
|
|
161
165
|
stdout,
|
|
@@ -172,6 +176,7 @@ export async function main(argv = process.argv.slice(2), io = { stdin: process.s
|
|
|
172
176
|
apply: options.apply,
|
|
173
177
|
permanent: options.permanent,
|
|
174
178
|
tools: options.tools,
|
|
179
|
+
workspaceDir: options.workspaceDir,
|
|
175
180
|
});
|
|
176
181
|
|
|
177
182
|
if (args.command === "clean" && !options.apply) {
|
package/src/protection.js
CHANGED
|
@@ -15,6 +15,7 @@ const PROTECTED_ROOT_NAMES = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
const PROTECTED_FILE_PATTERNS = [
|
|
18
|
+
/^api\b/i,
|
|
18
19
|
/^auth\b/i,
|
|
19
20
|
/^settings\b/i,
|
|
20
21
|
/^config\b/i,
|
|
@@ -100,6 +101,10 @@ function isCachePath(candidate, roots) {
|
|
|
100
101
|
path.join(roots.piAgentDir, "cache"),
|
|
101
102
|
path.join(roots.ompDir, "cache"),
|
|
102
103
|
path.join(roots.ompAgentDir, "cache"),
|
|
104
|
+
path.join(roots.copilotChatDir, "commandEmbeddings.json"),
|
|
105
|
+
path.join(roots.copilotChatDir, "settingEmbeddings.json"),
|
|
106
|
+
path.join(roots.copilotChatDir, "toolEmbeddingsCache.bin"),
|
|
107
|
+
path.join(roots.copilotChatDir, "copilot-cli-images"),
|
|
103
108
|
].some((root) => isInside(candidate, root));
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -112,6 +117,14 @@ export function isProtectedPath(candidate, roots) {
|
|
|
112
117
|
return false;
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
if (isInside(candidate, roots.copilotChatDir)) {
|
|
121
|
+
const absCandidate = absolute(candidate);
|
|
122
|
+
const absBase = absolute(roots.copilotChatDir);
|
|
123
|
+
if (absCandidate === path.join(absBase, "debugCommand")) return true;
|
|
124
|
+
if (absCandidate === path.join(absBase, "mcpServers.json")) return true;
|
|
125
|
+
return isProtectedUnderRoot(candidate, roots.copilotChatDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
return [roots.claudeDir, roots.piAgentDir, roots.ompAgentDir].some((root) => isProtectedUnderRoot(candidate, root));
|
|
116
129
|
}
|
|
117
130
|
|
package/src/scanner.js
CHANGED
|
@@ -10,6 +10,10 @@ function buildRoots(options) {
|
|
|
10
10
|
const piAgentDir = path.join(homeDir, ".pi", "agent");
|
|
11
11
|
const ompDir = path.join(homeDir, ".omp");
|
|
12
12
|
const ompAgentDir = path.join(ompDir, "agent");
|
|
13
|
+
const copilotChatDir =
|
|
14
|
+
process.platform === "darwin"
|
|
15
|
+
? path.join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage", "github.copilot-chat")
|
|
16
|
+
: path.join(homeDir, ".config", "Code", "User", "globalStorage", "github.copilot-chat");
|
|
13
17
|
|
|
14
18
|
return {
|
|
15
19
|
homeDir,
|
|
@@ -18,6 +22,7 @@ function buildRoots(options) {
|
|
|
18
22
|
piAgentDir,
|
|
19
23
|
ompDir,
|
|
20
24
|
ompAgentDir,
|
|
25
|
+
copilotChatDir,
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -54,6 +59,23 @@ function staticCandidates(options, roots) {
|
|
|
54
59
|
);
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
if (selected(options, "copilot")) {
|
|
63
|
+
for (const [dirName, category] of [
|
|
64
|
+
["ask-agent", "ask-agent"],
|
|
65
|
+
["plan-agent", "plan-agent"],
|
|
66
|
+
["explore-agent", "explore-agent"],
|
|
67
|
+
["logContextRecordings", "log-context"],
|
|
68
|
+
["memory-tool", "memory"],
|
|
69
|
+
]) {
|
|
70
|
+
candidates.push({ tool: "copilot", category, root: path.join(roots.copilotChatDir, dirName) });
|
|
71
|
+
}
|
|
72
|
+
candidates.push({ tool: "copilot", category: "sessions", root: path.join(roots.copilotChatDir, "copilot.cli.oldGlobalSessions.json") });
|
|
73
|
+
for (const name of ["commandEmbeddings.json", "settingEmbeddings.json", "toolEmbeddingsCache.bin"]) {
|
|
74
|
+
candidates.push({ tool: "copilot", category: "cache", root: path.join(roots.copilotChatDir, name) });
|
|
75
|
+
}
|
|
76
|
+
candidates.push({ tool: "copilot", category: "cache", root: path.join(roots.copilotChatDir, "copilot-cli-images") });
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
return candidates;
|
|
58
80
|
}
|
|
59
81
|
|
|
@@ -88,7 +110,74 @@ async function tempCandidates(options, roots, errors) {
|
|
|
88
110
|
return candidates;
|
|
89
111
|
}
|
|
90
112
|
|
|
91
|
-
function
|
|
113
|
+
async function copilotWorkspaceSessionCandidates(options, roots, errors) {
|
|
114
|
+
if (!selected(options, "copilot")) return [];
|
|
115
|
+
let dirents;
|
|
116
|
+
try {
|
|
117
|
+
dirents = await readdir(roots.copilotChatDir, { withFileTypes: true });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error?.code !== "ENOENT") errors.push(`Failed to read ${roots.copilotChatDir}: ${error.message}`);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return dirents
|
|
123
|
+
.filter(d => d.isFile() && d.name.startsWith("copilot.cli.workspaceSessions"))
|
|
124
|
+
.map(d => ({ tool: "copilot", category: "workspace-sessions", root: path.join(roots.copilotChatDir, d.name) }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function buildWorkspaceProjectIndex(workspaceDir, homeDir) {
|
|
128
|
+
const known = new Set();
|
|
129
|
+
known.add(workspaceDir.slice(homeDir.length).replace(/\//g, "-"));
|
|
130
|
+
|
|
131
|
+
let topDirents;
|
|
132
|
+
try {
|
|
133
|
+
topDirents = await readdir(workspaceDir, { withFileTypes: true });
|
|
134
|
+
} catch { return known; }
|
|
135
|
+
|
|
136
|
+
for (const dirent of topDirents) {
|
|
137
|
+
if (!dirent.isDirectory() || dirent.isSymbolicLink()) continue;
|
|
138
|
+
const p = path.join(workspaceDir, dirent.name);
|
|
139
|
+
known.add(p.slice(homeDir.length).replace(/\//g, "-"));
|
|
140
|
+
try {
|
|
141
|
+
const sub = await readdir(p, { withFileTypes: true });
|
|
142
|
+
for (const s of sub) {
|
|
143
|
+
if (!s.isDirectory() || s.isSymbolicLink()) continue;
|
|
144
|
+
const sp = path.join(p, s.name);
|
|
145
|
+
known.add(sp.slice(homeDir.length).replace(/\//g, "-"));
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
return known;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function findOrphanedOmpSessionDirs(roots, workspaceDir, errors) {
|
|
153
|
+
const orphaned = new Set();
|
|
154
|
+
const sessionsRoot = path.join(roots.ompAgentDir, "sessions");
|
|
155
|
+
let dirents;
|
|
156
|
+
try {
|
|
157
|
+
dirents = await readdir(sessionsRoot, { withFileTypes: true });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code !== "ENOENT") errors.push(`Failed to read ${sessionsRoot}: ${error.message}`);
|
|
160
|
+
return orphaned;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const workspaceEncoded = workspaceDir.slice(roots.homeDir.length).replace(/\//g, "-");
|
|
164
|
+
const prefix = workspaceEncoded + "-";
|
|
165
|
+
const known = await buildWorkspaceProjectIndex(workspaceDir, roots.homeDir);
|
|
166
|
+
|
|
167
|
+
for (const dirent of dirents) {
|
|
168
|
+
if (!dirent.isDirectory() || dirent.isSymbolicLink()) continue;
|
|
169
|
+
const name = dirent.name;
|
|
170
|
+
if (name !== workspaceEncoded && !name.startsWith(prefix)) continue;
|
|
171
|
+
if (name === prefix) continue;
|
|
172
|
+
if (!known.has(name)) {
|
|
173
|
+
orphaned.add(path.join(sessionsRoot, name));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return orphaned;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
function reasonFor({ cacheOptOut, protectedPath, oldEnough, isOrphaned }) {
|
|
92
181
|
if (cacheOptOut) {
|
|
93
182
|
return "cache cleanup opt-out";
|
|
94
183
|
}
|
|
@@ -97,6 +186,10 @@ function reasonFor({ cacheOptOut, protectedPath, oldEnough }) {
|
|
|
97
186
|
return "protected path";
|
|
98
187
|
}
|
|
99
188
|
|
|
189
|
+
if (isOrphaned) {
|
|
190
|
+
return "orphaned session";
|
|
191
|
+
}
|
|
192
|
+
|
|
100
193
|
if (!oldEnough) {
|
|
101
194
|
return "younger than threshold";
|
|
102
195
|
}
|
|
@@ -109,7 +202,10 @@ function entryFor(filePath, stats, candidate, roots, options) {
|
|
|
109
202
|
const cacheOptOut = classification.cache && !options.includeCache;
|
|
110
203
|
const protectedPath = classification.protected || isProtectedPath(filePath, roots);
|
|
111
204
|
const oldEnough = isOlderThan(stats.mtimeMs, options.nowMs, options.olderThanMs);
|
|
112
|
-
const
|
|
205
|
+
const isOrphaned = (options.orphanedDirs?.size ?? 0) > 0 &&
|
|
206
|
+
[...options.orphanedDirs].some(dir => filePath.startsWith(dir + path.sep));
|
|
207
|
+
const effectivelyOld = isOrphaned || oldEnough;
|
|
208
|
+
const action = protectedPath || cacheOptOut || !effectivelyOld ? "keep" : options.permanent ? "delete" : "trash";
|
|
113
209
|
|
|
114
210
|
return {
|
|
115
211
|
tool: candidate.tool,
|
|
@@ -119,7 +215,7 @@ function entryFor(filePath, stats, candidate, roots, options) {
|
|
|
119
215
|
sizeBytes: stats.size,
|
|
120
216
|
modifiedMs: stats.mtimeMs,
|
|
121
217
|
action,
|
|
122
|
-
reason: reasonFor({ cacheOptOut, protectedPath, oldEnough }),
|
|
218
|
+
reason: reasonFor({ cacheOptOut, protectedPath, oldEnough, isOrphaned }),
|
|
123
219
|
protected: protectedPath,
|
|
124
220
|
};
|
|
125
221
|
}
|
|
@@ -196,13 +292,21 @@ export async function scanRemnants(options) {
|
|
|
196
292
|
const roots = buildRoots(options);
|
|
197
293
|
const errors = [];
|
|
198
294
|
const entries = [];
|
|
295
|
+
|
|
296
|
+
const orphanedDirs = options.workspaceDir
|
|
297
|
+
? await findOrphanedOmpSessionDirs(roots, options.workspaceDir, errors)
|
|
298
|
+
: new Set();
|
|
299
|
+
|
|
300
|
+
const effectiveOptions = { ...options, orphanedDirs };
|
|
301
|
+
|
|
199
302
|
const candidates = [
|
|
200
|
-
...staticCandidates(
|
|
201
|
-
...(await tempCandidates(
|
|
303
|
+
...staticCandidates(effectiveOptions, roots),
|
|
304
|
+
...(await tempCandidates(effectiveOptions, roots, errors)),
|
|
305
|
+
...(await copilotWorkspaceSessionCandidates(effectiveOptions, roots, errors)),
|
|
202
306
|
];
|
|
203
307
|
|
|
204
308
|
for (const candidate of candidates) {
|
|
205
|
-
await walkCandidate(candidate, roots,
|
|
309
|
+
await walkCandidate(candidate, roots, effectiveOptions, entries, errors);
|
|
206
310
|
}
|
|
207
311
|
|
|
208
312
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|