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 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
- agent-session-kill # interactive picker
36
- agentkill # short alias
37
- agent-session-kill interactive # explicit picker
38
- agent-session-kill scan --older-than 14d # non-interactive inventory
39
- agent-session-kill scan --json # machine-readable inventory
40
- agent-session-kill clean --dry-run # non-interactive dry run
41
- agent-session-kill clean --apply # apply non-interactive manifest
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 `temp`. Repeat or comma-separate. | all |
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.1.0",
4
- "description": "NPKILL-style cleanup for Claude, Pi, and OMP agent session remnants.",
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 OMP agent session remnants")
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 temp; repeat or comma-separate", collectTools, [])
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 reasonFor({ cacheOptOut, protectedPath, oldEnough }) {
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 action = protectedPath || cacheOptOut || !oldEnough ? "keep" : options.permanent ? "delete" : "trash";
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(options, roots),
201
- ...(await tempCandidates(options, roots, errors)),
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, options, entries, errors);
309
+ await walkCandidate(candidate, roots, effectiveOptions, entries, errors);
206
310
  }
207
311
 
208
312
  entries.sort((a, b) => a.path.localeCompare(b.path));