token-pilot 0.19.2 → 0.22.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.
Files changed (89) hide show
  1. package/.claude-plugin/hooks/hooks.json +21 -0
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +172 -315
  5. package/dist/agents/tp-commit-writer.md +41 -0
  6. package/dist/agents/tp-dead-code-finder.md +43 -0
  7. package/dist/agents/tp-debugger.md +45 -0
  8. package/dist/agents/tp-impact-analyzer.md +44 -0
  9. package/dist/agents/tp-migration-scout.md +43 -0
  10. package/dist/agents/tp-onboard.md +40 -0
  11. package/dist/agents/tp-pr-reviewer.md +41 -0
  12. package/dist/agents/tp-refactor-planner.md +42 -0
  13. package/dist/agents/tp-run.md +48 -0
  14. package/dist/agents/tp-test-triage.md +40 -0
  15. package/dist/agents/tp-test-writer.md +46 -0
  16. package/dist/cli/agent-frontmatter.d.ts +48 -0
  17. package/dist/cli/agent-frontmatter.js +189 -0
  18. package/dist/cli/bless-agents.d.ts +65 -0
  19. package/dist/cli/bless-agents.js +307 -0
  20. package/dist/cli/claudeignore.d.ts +33 -0
  21. package/dist/cli/claudeignore.js +88 -0
  22. package/dist/cli/claudemd-hygiene.d.ts +26 -0
  23. package/dist/cli/claudemd-hygiene.js +43 -0
  24. package/dist/cli/doctor-drift.d.ts +31 -0
  25. package/dist/cli/doctor-drift.js +130 -0
  26. package/dist/cli/doctor-env-check.d.ts +25 -0
  27. package/dist/cli/doctor-env-check.js +91 -0
  28. package/dist/cli/install-agents.d.ts +108 -0
  29. package/dist/cli/install-agents.js +402 -0
  30. package/dist/cli/save-doc.d.ts +42 -0
  31. package/dist/cli/save-doc.js +145 -0
  32. package/dist/cli/scan-agents.d.ts +46 -0
  33. package/dist/cli/scan-agents.js +227 -0
  34. package/dist/cli/stats.d.ts +36 -0
  35. package/dist/cli/stats.js +131 -0
  36. package/dist/cli/unbless-agents.d.ts +33 -0
  37. package/dist/cli/unbless-agents.js +85 -0
  38. package/dist/cli/uninstall-agents.d.ts +36 -0
  39. package/dist/cli/uninstall-agents.js +117 -0
  40. package/dist/config/defaults.d.ts +1 -1
  41. package/dist/config/defaults.js +14 -8
  42. package/dist/config/loader.d.ts +1 -1
  43. package/dist/config/loader.js +105 -11
  44. package/dist/core/context-registry.d.ts +16 -1
  45. package/dist/core/context-registry.js +60 -28
  46. package/dist/core/event-log.d.ts +79 -0
  47. package/dist/core/event-log.js +190 -0
  48. package/dist/core/session-registry.d.ts +43 -0
  49. package/dist/core/session-registry.js +113 -0
  50. package/dist/core/session-savings.d.ts +19 -0
  51. package/dist/core/session-savings.js +60 -0
  52. package/dist/handlers/session-budget.d.ts +32 -0
  53. package/dist/handlers/session-budget.js +61 -0
  54. package/dist/handlers/session-snapshot-persist.d.ts +22 -0
  55. package/dist/handlers/session-snapshot-persist.js +76 -0
  56. package/dist/hooks/adaptive-threshold.d.ts +27 -0
  57. package/dist/hooks/adaptive-threshold.js +46 -0
  58. package/dist/hooks/format-deny-message.d.ts +21 -0
  59. package/dist/hooks/format-deny-message.js +147 -0
  60. package/dist/hooks/installer.js +121 -31
  61. package/dist/hooks/path-safety.d.ts +16 -0
  62. package/dist/hooks/path-safety.js +34 -0
  63. package/dist/hooks/post-bash.d.ts +46 -0
  64. package/dist/hooks/post-bash.js +77 -0
  65. package/dist/hooks/session-start.d.ts +45 -0
  66. package/dist/hooks/session-start.js +179 -0
  67. package/dist/hooks/summary-ast-index.d.ts +28 -0
  68. package/dist/hooks/summary-ast-index.js +122 -0
  69. package/dist/hooks/summary-head-tail.d.ts +15 -0
  70. package/dist/hooks/summary-head-tail.js +78 -0
  71. package/dist/hooks/summary-pipeline.d.ts +35 -0
  72. package/dist/hooks/summary-pipeline.js +63 -0
  73. package/dist/hooks/summary-regex.d.ts +14 -0
  74. package/dist/hooks/summary-regex.js +130 -0
  75. package/dist/hooks/summary-types.d.ts +29 -0
  76. package/dist/hooks/summary-types.js +9 -0
  77. package/dist/index.d.ts +15 -3
  78. package/dist/index.js +509 -149
  79. package/dist/integration/context-mode-detector.d.ts +7 -1
  80. package/dist/integration/context-mode-detector.js +51 -15
  81. package/dist/server/tool-definitions.d.ts +149 -0
  82. package/dist/server/tool-definitions.js +424 -202
  83. package/dist/server.d.ts +1 -1
  84. package/dist/server.js +456 -179
  85. package/dist/templates/agent-builder.d.ts +49 -0
  86. package/dist/templates/agent-builder.js +104 -0
  87. package/dist/types.d.ts +38 -4
  88. package/package.json +4 -2
  89. package/skills/stats/SKILL.md +13 -2
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Agent scanner + classifier (subtasks 3.2 + 3.3).
3
+ *
4
+ * Discovers agent .md files from three locations:
5
+ * 1. project .claude/agents/**\/*.md (scope: 'project')
6
+ * 2. user ~/.claude/agents/**\/*.md (scope: 'user')
7
+ * 3. plugin-contributed agents (scope: 'plugin')
8
+ *
9
+ * For each file: parses frontmatter, computes body hash, returns a ScannedAgent.
10
+ * Never throws — bad/unreadable files are skipped with a one-line stderr note.
11
+ * Symlinks pointing outside the scope's nominal root are skipped.
12
+ */
13
+ import { readFile, lstat, realpath, readdir } from "node:fs/promises";
14
+ import { createHash } from "node:crypto";
15
+ import { join, resolve, basename } from "node:path";
16
+ import { parseFrontmatter, parseToolsField, } from "./agent-frontmatter.js";
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+ function sha256(s) {
19
+ return createHash("sha256").update(s).digest("hex");
20
+ }
21
+ /**
22
+ * Recursively list all *.md files under a directory.
23
+ * Returns absolute paths. Returns [] if the directory doesn't exist.
24
+ */
25
+ async function listMdFiles(dir) {
26
+ const results = [];
27
+ let entries;
28
+ try {
29
+ entries = await readdir(dir, { withFileTypes: true, encoding: "utf-8" });
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ for (const entry of entries) {
35
+ const name = entry.name;
36
+ const fullPath = join(dir, name);
37
+ if (entry.isDirectory()) {
38
+ const nested = await listMdFiles(fullPath);
39
+ results.push(...nested);
40
+ }
41
+ else if (entry.isFile() && name.endsWith(".md")) {
42
+ results.push(fullPath);
43
+ }
44
+ // Skip symlinks entirely at this level — handled per-file below
45
+ }
46
+ return results;
47
+ }
48
+ /**
49
+ * Expand glob patterns (supporting only the trailing `*.md` form used for
50
+ * plugin cache paths). For patterns without wildcards the path is treated
51
+ * literally. Never throws.
52
+ */
53
+ async function resolveGlobs(patterns) {
54
+ const results = [];
55
+ for (const pattern of patterns) {
56
+ if (!pattern.includes("*")) {
57
+ // Literal file path
58
+ try {
59
+ const stat = await lstat(pattern);
60
+ if (stat.isFile())
61
+ results.push(resolve(pattern));
62
+ }
63
+ catch {
64
+ // ignore missing
65
+ }
66
+ continue;
67
+ }
68
+ // Only handle trailing `*.md` glob (the only form used for plugin dirs)
69
+ const starIdx = pattern.indexOf("*");
70
+ const dir = pattern.slice(0, starIdx);
71
+ const suffix = pattern.slice(starIdx + 1); // e.g. ".md"
72
+ let entries;
73
+ try {
74
+ entries = await readdir(dir.replace(/\/$/, ""), {
75
+ withFileTypes: true,
76
+ encoding: "utf-8",
77
+ });
78
+ }
79
+ catch {
80
+ continue;
81
+ }
82
+ for (const entry of entries) {
83
+ const name = entry.name;
84
+ if (entry.isFile() && name.endsWith(suffix)) {
85
+ results.push(resolve(join(dir.replace(/\/$/, ""), name)));
86
+ }
87
+ }
88
+ }
89
+ return results;
90
+ }
91
+ /**
92
+ * Check whether filePath is a symlink that resolves outside of rootDir.
93
+ * Returns true if the file should be skipped.
94
+ */
95
+ async function isSymlinkOutsideRoot(filePath, rootDir) {
96
+ let stat;
97
+ try {
98
+ stat = await lstat(filePath);
99
+ }
100
+ catch {
101
+ return false;
102
+ }
103
+ if (!stat.isSymbolicLink())
104
+ return false;
105
+ try {
106
+ const real = await realpath(filePath);
107
+ const normalRoot = resolve(rootDir) + "/";
108
+ return !real.startsWith(normalRoot);
109
+ }
110
+ catch {
111
+ // If we can't resolve, skip it to be safe
112
+ return true;
113
+ }
114
+ }
115
+ /**
116
+ * Parse one agent file. Returns a ScannedAgent or null if the file should be
117
+ * skipped (parse failure, missing name, symlink outside root).
118
+ */
119
+ async function parseAgentFile(filePath, scope, scopeRoot) {
120
+ // Symlink guard
121
+ if (await isSymlinkOutsideRoot(filePath, scopeRoot)) {
122
+ return null;
123
+ }
124
+ let content;
125
+ try {
126
+ content = await readFile(filePath, "utf-8");
127
+ }
128
+ catch (err) {
129
+ process.stderr.write(`token-pilot scan-agents: skipping ${filePath}: ${err instanceof Error ? err.message : err}\n`);
130
+ return null;
131
+ }
132
+ const { meta, body } = parseFrontmatter(content);
133
+ // Must have a name
134
+ const name = typeof meta.name === "string" && meta.name.trim()
135
+ ? meta.name.trim()
136
+ : basename(filePath, ".md");
137
+ // If body is empty AND no name in frontmatter AND no description, it's likely
138
+ // a file with no frontmatter at all — skip it
139
+ if (!meta.name && !meta.description && body === content) {
140
+ process.stderr.write(`token-pilot scan-agents: skipping ${filePath}: no frontmatter found\n`);
141
+ return null;
142
+ }
143
+ const tools = parseToolsField(meta.tools);
144
+ const description = typeof meta.description === "string" ? meta.description : "";
145
+ const bodyHash = sha256(body);
146
+ // blessed: check token_pilot.blessed === true (explicit marker, not substring)
147
+ const blessed = meta.token_pilot !== null &&
148
+ typeof meta.token_pilot === "object" &&
149
+ meta.token_pilot.blessed === true;
150
+ return {
151
+ name,
152
+ path: filePath,
153
+ scope,
154
+ tools,
155
+ description,
156
+ bodyHash,
157
+ blessed,
158
+ };
159
+ }
160
+ // ─── scanAgents ───────────────────────────────────────────────────────────────
161
+ /**
162
+ * Scan all agent directories and return parsed ScannedAgent entries.
163
+ * Never throws.
164
+ */
165
+ export async function scanAgents(opts) {
166
+ const results = [];
167
+ // 1. Project .claude/agents/
168
+ const projectAgentsDir = join(opts.projectRoot, ".claude", "agents");
169
+ const projectFiles = await listMdFiles(projectAgentsDir);
170
+ for (const filePath of projectFiles) {
171
+ const agent = await parseAgentFile(filePath, "project", projectAgentsDir);
172
+ if (agent)
173
+ results.push(agent);
174
+ }
175
+ // 2. User ~/.claude/agents/
176
+ const userAgentsDir = join(opts.homeDir, ".claude", "agents");
177
+ const userFiles = await listMdFiles(userAgentsDir);
178
+ for (const filePath of userFiles) {
179
+ const agent = await parseAgentFile(filePath, "user", userAgentsDir);
180
+ if (agent)
181
+ results.push(agent);
182
+ }
183
+ // 3. Plugin cache globs
184
+ const pluginFiles = await resolveGlobs(opts.pluginCacheGlob);
185
+ for (const filePath of pluginFiles) {
186
+ // For plugin files, use the file's parent directory as the scope root
187
+ const pluginRoot = resolve(filePath, "..");
188
+ const agent = await parseAgentFile(filePath, "plugin", pluginRoot);
189
+ if (agent)
190
+ results.push(agent);
191
+ }
192
+ return results;
193
+ }
194
+ // ─── classifyAgent ────────────────────────────────────────────────────────────
195
+ const TP_PREFIX = "mcp__token-pilot__";
196
+ function hasTokenPilotTool(tools) {
197
+ return tools.some((t) => t.startsWith(TP_PREFIX));
198
+ }
199
+ /**
200
+ * Classify an agent by its tools field.
201
+ *
202
+ * A — wildcard (tools: * | All tools) → already has MCP access
203
+ * A — explicit list that already contains mcp__token-pilot__* → already has access
204
+ * B — exclusion form where mcp__token-pilot__ is NOT excluded → has access
205
+ * C — explicit list without mcp__token-pilot__* → candidate for blessing
206
+ * C — exclusion form that explicitly excludes mcp__token-pilot__* → needs blessing
207
+ */
208
+ export function classifyAgent(agent) {
209
+ const { tools } = agent;
210
+ switch (tools.kind) {
211
+ case "wildcard":
212
+ return "A";
213
+ case "exclusion": {
214
+ // If the exclusion list mentions mcp__token-pilot__ → agent lacks access
215
+ const excludesTP = tools.excluded.some((e) => e.startsWith(TP_PREFIX));
216
+ return excludesTP ? "C" : "B";
217
+ }
218
+ case "explicit": {
219
+ // If the explicit list already contains mcp__token-pilot__ → treat as A
220
+ if (hasTokenPilotTool(tools.tools))
221
+ return "A";
222
+ // Otherwise it's a restricted list → C candidate
223
+ return "C";
224
+ }
225
+ }
226
+ }
227
+ //# sourceMappingURL=scan-agents.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Phase 6 subtask 6.3 — `token-pilot stats` CLI.
3
+ *
4
+ * Reads from `<projectRoot>/.token-pilot/hook-events.jsonl` and renders
5
+ * one of three views:
6
+ * - default : totals + top files by savedTokens
7
+ * - --session : events filtered to a single session_id (explicit arg
8
+ * or the most recent session in the log)
9
+ * - --by-agent : events grouped by agent_type, sorted desc by savedTokens
10
+ *
11
+ * formatStats() is pure (events in → string out) so tests drive it
12
+ * directly without touching the filesystem.
13
+ */
14
+ import { type HookEvent } from "../core/event-log.js";
15
+ export interface StatsOptions {
16
+ /**
17
+ * `true` → pick the session_id of the most recent event.
18
+ * `string` → filter to that specific session_id.
19
+ * `undefined` → no session filter.
20
+ */
21
+ session?: boolean | string;
22
+ byAgent?: boolean;
23
+ }
24
+ /**
25
+ * Pure formatter. Takes the full event list and options; returns the
26
+ * rendered text block. Multi-line; no trailing newline.
27
+ */
28
+ export declare function formatStats(events: HookEvent[], opts: StatsOptions): string;
29
+ /**
30
+ * CLI entry: `token-pilot stats [--session[=<id>]] [--by-agent]`.
31
+ * Prints to stdout and returns exit code 0.
32
+ */
33
+ export declare function handleStats(argv: string[], opts?: {
34
+ projectRoot?: string;
35
+ }): Promise<number>;
36
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Phase 6 subtask 6.3 — `token-pilot stats` CLI.
3
+ *
4
+ * Reads from `<projectRoot>/.token-pilot/hook-events.jsonl` and renders
5
+ * one of three views:
6
+ * - default : totals + top files by savedTokens
7
+ * - --session : events filtered to a single session_id (explicit arg
8
+ * or the most recent session in the log)
9
+ * - --by-agent : events grouped by agent_type, sorted desc by savedTokens
10
+ *
11
+ * formatStats() is pure (events in → string out) so tests drive it
12
+ * directly without touching the filesystem.
13
+ */
14
+ import { loadEvents } from "../core/event-log.js";
15
+ function sumSaved(events) {
16
+ return events.reduce((sum, e) => sum + e.savedTokens, 0);
17
+ }
18
+ function groupBy(events, keyOf) {
19
+ const out = new Map();
20
+ for (const e of events) {
21
+ const k = keyOf(e);
22
+ const bucket = out.get(k);
23
+ if (bucket)
24
+ bucket.push(e);
25
+ else
26
+ out.set(k, [e]);
27
+ }
28
+ return out;
29
+ }
30
+ function pad(label, width) {
31
+ return label.length >= width
32
+ ? label
33
+ : label + " ".repeat(width - label.length);
34
+ }
35
+ function pickMostRecentSession(events) {
36
+ if (events.length === 0)
37
+ return null;
38
+ let latest = events[0];
39
+ for (const e of events)
40
+ if (e.ts > latest.ts)
41
+ latest = e;
42
+ return latest.session_id;
43
+ }
44
+ /**
45
+ * Pure formatter. Takes the full event list and options; returns the
46
+ * rendered text block. Multi-line; no trailing newline.
47
+ */
48
+ export function formatStats(events, opts) {
49
+ let scope = events;
50
+ let sessionLabel = null;
51
+ // --session filter
52
+ if (opts.session !== undefined) {
53
+ const target = opts.session === true ? pickMostRecentSession(events) : opts.session;
54
+ if (!target) {
55
+ return "No events yet.";
56
+ }
57
+ sessionLabel = target;
58
+ scope = events.filter((e) => e.session_id === target);
59
+ if (scope.length === 0) {
60
+ return `No events for session ${target}.`;
61
+ }
62
+ }
63
+ if (scope.length === 0) {
64
+ return "No events yet.";
65
+ }
66
+ const lines = [];
67
+ const total = sumSaved(scope);
68
+ const sessionSuffix = sessionLabel ? ` (session ${sessionLabel})` : "";
69
+ lines.push(`token-pilot stats${sessionSuffix} — ${scope.length} event${scope.length === 1 ? "" : "s"}, ~${total} tokens saved`);
70
+ if (opts.byAgent) {
71
+ // Group by agent_type (null → "main").
72
+ const groups = groupBy(scope, (e) => (e.agent_type ?? "main"));
73
+ const rows = [...groups.entries()]
74
+ .map(([agent, evs]) => ({
75
+ agent,
76
+ saved: sumSaved(evs),
77
+ count: evs.length,
78
+ }))
79
+ .sort((a, b) => b.saved - a.saved);
80
+ lines.push("");
81
+ lines.push("By agent:");
82
+ for (const r of rows) {
83
+ lines.push(` ${pad(r.agent, 20)} ${r.count.toString().padStart(4)}× events ~${r.saved} tokens saved`);
84
+ }
85
+ }
86
+ else {
87
+ // Default view: top files by savedTokens.
88
+ const groups = groupBy(scope, (e) => e.file);
89
+ const rows = [...groups.entries()]
90
+ .map(([file, evs]) => ({
91
+ file,
92
+ saved: sumSaved(evs),
93
+ count: evs.length,
94
+ }))
95
+ .sort((a, b) => b.saved - a.saved)
96
+ .slice(0, 10);
97
+ lines.push("");
98
+ lines.push("Top files:");
99
+ for (const r of rows) {
100
+ lines.push(` ${pad(r.file, 40)} ${r.count.toString().padStart(3)}× ~${r.saved} tokens saved`);
101
+ }
102
+ }
103
+ return lines.join("\n");
104
+ }
105
+ // ─── CLI wrapper ─────────────────────────────────────────────────────────────
106
+ function parseFlag(argv, key) {
107
+ for (const a of argv) {
108
+ if (a === `--${key}`)
109
+ return true;
110
+ if (a.startsWith(`--${key}=`))
111
+ return a.slice(key.length + 3);
112
+ }
113
+ return undefined;
114
+ }
115
+ /**
116
+ * CLI entry: `token-pilot stats [--session[=<id>]] [--by-agent]`.
117
+ * Prints to stdout and returns exit code 0.
118
+ */
119
+ export async function handleStats(argv, opts) {
120
+ const projectRoot = opts?.projectRoot ?? process.cwd();
121
+ const events = await loadEvents(projectRoot);
122
+ const session = parseFlag(argv, "session");
123
+ const byAgent = parseFlag(argv, "by-agent");
124
+ const rendered = formatStats(events, {
125
+ session: session === undefined ? undefined : session,
126
+ byAgent: byAgent === true,
127
+ });
128
+ process.stdout.write(rendered + "\n");
129
+ return 0;
130
+ }
131
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reverse side of `bless-agents` (Phase 3 subtask 3.6).
3
+ *
4
+ * Walks `./.claude/agents/*.md` in the project, parses each file's
5
+ * frontmatter, and deletes only files that carry the explicit
6
+ * `token_pilot.blessed: true` marker. Files without that marker — user's
7
+ * own customised agents, other plugins' overrides — are left untouched.
8
+ *
9
+ * The function never throws out: every I/O failure is captured in the
10
+ * returned summary so the caller can surface one human-readable stderr
11
+ * message afterwards.
12
+ */
13
+ export interface UnblessOptions {
14
+ /** Project root — .claude/agents is resolved relative to this. */
15
+ projectRoot: string;
16
+ /** Specific agent names (without ".md"). Ignored when `all` is true. */
17
+ names: string[];
18
+ /** When true, remove every blessed file regardless of `names`. */
19
+ all: boolean;
20
+ }
21
+ export interface UnblessSummary {
22
+ /** Count of files deleted. */
23
+ removed: number;
24
+ /** Count of files intentionally skipped (missing, not blessed, etc.). */
25
+ skipped: number;
26
+ /** Details for skipped entries; useful for stderr reporting. */
27
+ skipDetails: Array<{
28
+ name: string;
29
+ reason: string;
30
+ }>;
31
+ }
32
+ export declare function unblessAgents(opts: UnblessOptions): Promise<UnblessSummary>;
33
+ //# sourceMappingURL=unbless-agents.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Reverse side of `bless-agents` (Phase 3 subtask 3.6).
3
+ *
4
+ * Walks `./.claude/agents/*.md` in the project, parses each file's
5
+ * frontmatter, and deletes only files that carry the explicit
6
+ * `token_pilot.blessed: true` marker. Files without that marker — user's
7
+ * own customised agents, other plugins' overrides — are left untouched.
8
+ *
9
+ * The function never throws out: every I/O failure is captured in the
10
+ * returned summary so the caller can surface one human-readable stderr
11
+ * message afterwards.
12
+ */
13
+ import { readdir, readFile, unlink } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+ import { parseFrontmatter } from "./agent-frontmatter.js";
16
+ function isBlessedMarker(meta) {
17
+ const tp = meta.token_pilot;
18
+ if (typeof tp !== "object" || tp === null)
19
+ return false;
20
+ return tp.blessed === true;
21
+ }
22
+ export async function unblessAgents(opts) {
23
+ const summary = { removed: 0, skipped: 0, skipDetails: [] };
24
+ const agentsDir = join(opts.projectRoot, ".claude", "agents");
25
+ let entries;
26
+ try {
27
+ entries = await readdir(agentsDir);
28
+ }
29
+ catch {
30
+ // No .claude/agents dir → nothing to do; treat as success.
31
+ return summary;
32
+ }
33
+ const files = entries.filter((f) => f.endsWith(".md"));
34
+ const filesByName = new Map(files.map((f) => [f.replace(/\.md$/, ""), f]));
35
+ const targetNames = opts.all
36
+ ? files.map((f) => f.replace(/\.md$/, ""))
37
+ : opts.names;
38
+ for (const name of targetNames) {
39
+ const fileName = filesByName.get(name);
40
+ if (!fileName) {
41
+ summary.skipped++;
42
+ summary.skipDetails.push({ name, reason: "not found" });
43
+ continue;
44
+ }
45
+ const fullPath = join(agentsDir, fileName);
46
+ let body;
47
+ try {
48
+ body = await readFile(fullPath, "utf-8");
49
+ }
50
+ catch {
51
+ summary.skipped++;
52
+ summary.skipDetails.push({ name, reason: "read error" });
53
+ continue;
54
+ }
55
+ let meta;
56
+ try {
57
+ ({ meta } = parseFrontmatter(body));
58
+ }
59
+ catch {
60
+ summary.skipped++;
61
+ summary.skipDetails.push({ name, reason: "malformed frontmatter" });
62
+ continue;
63
+ }
64
+ if (!isBlessedMarker(meta)) {
65
+ summary.skipped++;
66
+ summary.skipDetails.push({ name, reason: "no blessed marker" });
67
+ continue;
68
+ }
69
+ try {
70
+ await unlink(fullPath);
71
+ summary.removed++;
72
+ }
73
+ catch {
74
+ summary.skipped++;
75
+ summary.skipDetails.push({ name, reason: "delete failed" });
76
+ }
77
+ }
78
+ // Emit one human-readable stderr line when we actually removed something.
79
+ if (summary.removed > 0) {
80
+ const plural = summary.removed === 1 ? "agent" : "agents";
81
+ process.stderr.write(`[token-pilot] Unblessed ${summary.removed} ${plural}.\n`);
82
+ }
83
+ return summary;
84
+ }
85
+ //# sourceMappingURL=unbless-agents.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Phase 5 subtask 5.5 — uninstall tp-* agents.
3
+ *
4
+ * Removes only files in the target scope's `.claude/agents/` that have
5
+ * `token_pilot_body_hash` in their frontmatter. Files without the marker
6
+ * are user-owned and are never touched. Scope is required — no global
7
+ * default — to prevent accidental deletion from the wrong location.
8
+ *
9
+ * Symmetric to install-agents: separation of core (uninstallAgents) and
10
+ * CLI wrapper (handleUninstallAgents) mirrors the Phase 3 bless/unbless
11
+ * split.
12
+ */
13
+ export type Scope = "user" | "project";
14
+ export interface UninstallOptions {
15
+ scope: Scope;
16
+ projectRoot: string;
17
+ homeDir: string;
18
+ }
19
+ export interface UninstallResult {
20
+ removed: string[];
21
+ skipped: Array<{
22
+ name: string;
23
+ reason: string;
24
+ }>;
25
+ targetDir: string;
26
+ }
27
+ export declare function uninstallAgents(opts: UninstallOptions): Promise<UninstallResult>;
28
+ /**
29
+ * CLI entry: `token-pilot uninstall-agents --scope=user|project`.
30
+ * Returns the exit code (0 success, 1 error).
31
+ */
32
+ export declare function handleUninstallAgents(argv: string[], opts?: {
33
+ homeDir?: string;
34
+ projectRoot?: string;
35
+ }): Promise<number>;
36
+ //# sourceMappingURL=uninstall-agents.d.ts.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Phase 5 subtask 5.5 — uninstall tp-* agents.
3
+ *
4
+ * Removes only files in the target scope's `.claude/agents/` that have
5
+ * `token_pilot_body_hash` in their frontmatter. Files without the marker
6
+ * are user-owned and are never touched. Scope is required — no global
7
+ * default — to prevent accidental deletion from the wrong location.
8
+ *
9
+ * Symmetric to install-agents: separation of core (uninstallAgents) and
10
+ * CLI wrapper (handleUninstallAgents) mirrors the Phase 3 bless/unbless
11
+ * split.
12
+ */
13
+ import { readdir, readFile, unlink } from "node:fs/promises";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { parseFrontmatter } from "./agent-frontmatter.js";
17
+ function targetDirFor(opts) {
18
+ const root = opts.scope === "user" ? opts.homeDir : opts.projectRoot;
19
+ return join(root, ".claude", "agents");
20
+ }
21
+ function hasTpHashMarker(meta) {
22
+ const h = meta.token_pilot_body_hash;
23
+ return typeof h === "string" && h.length > 0;
24
+ }
25
+ export async function uninstallAgents(opts) {
26
+ const target = targetDirFor(opts);
27
+ const result = {
28
+ removed: [],
29
+ skipped: [],
30
+ targetDir: target,
31
+ };
32
+ let entries;
33
+ try {
34
+ entries = await readdir(target);
35
+ }
36
+ catch {
37
+ // Target dir missing → nothing to do.
38
+ return result;
39
+ }
40
+ const tpFiles = entries.filter((f) => f.endsWith(".md") && f.startsWith("tp-"));
41
+ for (const entry of tpFiles) {
42
+ const name = entry.replace(/\.md$/, "");
43
+ const fullPath = join(target, entry);
44
+ let md;
45
+ try {
46
+ md = await readFile(fullPath, "utf-8");
47
+ }
48
+ catch {
49
+ result.skipped.push({ name, reason: "read failed" });
50
+ continue;
51
+ }
52
+ let meta;
53
+ try {
54
+ ({ meta } = parseFrontmatter(md));
55
+ }
56
+ catch {
57
+ result.skipped.push({ name, reason: "malformed frontmatter" });
58
+ continue;
59
+ }
60
+ if (!hasTpHashMarker(meta)) {
61
+ result.skipped.push({
62
+ name,
63
+ reason: "not installed by token-pilot (no token_pilot_body_hash)",
64
+ });
65
+ continue;
66
+ }
67
+ try {
68
+ await unlink(fullPath);
69
+ result.removed.push(name);
70
+ }
71
+ catch {
72
+ result.skipped.push({ name, reason: "delete failed" });
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ // ─── CLI wrapper ─────────────────────────────────────────────────────────────
78
+ function parseFlag(argv, key) {
79
+ for (const a of argv) {
80
+ if (a === `--${key}`)
81
+ return "true";
82
+ if (a.startsWith(`--${key}=`))
83
+ return a.slice(key.length + 3);
84
+ }
85
+ return undefined;
86
+ }
87
+ /**
88
+ * CLI entry: `token-pilot uninstall-agents --scope=user|project`.
89
+ * Returns the exit code (0 success, 1 error).
90
+ */
91
+ export async function handleUninstallAgents(argv, opts) {
92
+ const scopeArg = parseFlag(argv, "scope");
93
+ if (scopeArg !== "user" && scopeArg !== "project") {
94
+ process.stderr.write("Usage: token-pilot uninstall-agents --scope=user|project\n");
95
+ return 1;
96
+ }
97
+ const result = await uninstallAgents({
98
+ scope: scopeArg,
99
+ projectRoot: opts?.projectRoot ?? process.cwd(),
100
+ homeDir: opts?.homeDir ?? homedir(),
101
+ });
102
+ if (result.removed.length > 0) {
103
+ const plural = result.removed.length === 1 ? "agent" : "agents";
104
+ process.stderr.write(`[token-pilot] Removed ${result.removed.length} ${plural} from ${result.targetDir}.\n`);
105
+ }
106
+ if (result.skipped.length > 0) {
107
+ process.stderr.write(`[token-pilot] Skipped ${result.skipped.length}:\n`);
108
+ for (const s of result.skipped) {
109
+ process.stderr.write(` - ${s.name}: ${s.reason}\n`);
110
+ }
111
+ }
112
+ if (result.removed.length === 0 && result.skipped.length === 0) {
113
+ process.stderr.write(`[token-pilot] No tp-* agents found in ${result.targetDir}.\n`);
114
+ }
115
+ return 0;
116
+ }
117
+ //# sourceMappingURL=uninstall-agents.js.map
@@ -1,3 +1,3 @@
1
- import type { TokenPilotConfig } from '../types.js';
1
+ import type { TokenPilotConfig } from "../types.js";
2
2
  export declare const DEFAULT_CONFIG: TokenPilotConfig;
3
3
  //# sourceMappingURL=defaults.d.ts.map