token-pilot 0.19.1 → 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 (94) hide show
  1. package/.claude-plugin/hooks/hooks.json +21 -0
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +736 -580
  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/ast-index/binary-manager.d.ts +3 -3
  17. package/dist/ast-index/binary-manager.js +74 -11
  18. package/dist/ast-index/client.d.ts +5 -1
  19. package/dist/ast-index/client.js +9 -2
  20. package/dist/cli/agent-frontmatter.d.ts +48 -0
  21. package/dist/cli/agent-frontmatter.js +189 -0
  22. package/dist/cli/bless-agents.d.ts +65 -0
  23. package/dist/cli/bless-agents.js +307 -0
  24. package/dist/cli/claudeignore.d.ts +33 -0
  25. package/dist/cli/claudeignore.js +88 -0
  26. package/dist/cli/claudemd-hygiene.d.ts +26 -0
  27. package/dist/cli/claudemd-hygiene.js +43 -0
  28. package/dist/cli/doctor-drift.d.ts +31 -0
  29. package/dist/cli/doctor-drift.js +130 -0
  30. package/dist/cli/doctor-env-check.d.ts +25 -0
  31. package/dist/cli/doctor-env-check.js +91 -0
  32. package/dist/cli/install-agents.d.ts +108 -0
  33. package/dist/cli/install-agents.js +402 -0
  34. package/dist/cli/save-doc.d.ts +42 -0
  35. package/dist/cli/save-doc.js +145 -0
  36. package/dist/cli/scan-agents.d.ts +46 -0
  37. package/dist/cli/scan-agents.js +227 -0
  38. package/dist/cli/stats.d.ts +36 -0
  39. package/dist/cli/stats.js +131 -0
  40. package/dist/cli/unbless-agents.d.ts +33 -0
  41. package/dist/cli/unbless-agents.js +85 -0
  42. package/dist/cli/uninstall-agents.d.ts +36 -0
  43. package/dist/cli/uninstall-agents.js +117 -0
  44. package/dist/config/defaults.d.ts +1 -1
  45. package/dist/config/defaults.js +14 -8
  46. package/dist/config/loader.d.ts +1 -1
  47. package/dist/config/loader.js +105 -11
  48. package/dist/core/context-registry.d.ts +16 -1
  49. package/dist/core/context-registry.js +60 -28
  50. package/dist/core/event-log.d.ts +79 -0
  51. package/dist/core/event-log.js +190 -0
  52. package/dist/core/session-registry.d.ts +43 -0
  53. package/dist/core/session-registry.js +113 -0
  54. package/dist/core/session-savings.d.ts +19 -0
  55. package/dist/core/session-savings.js +60 -0
  56. package/dist/handlers/session-budget.d.ts +32 -0
  57. package/dist/handlers/session-budget.js +61 -0
  58. package/dist/handlers/session-snapshot-persist.d.ts +22 -0
  59. package/dist/handlers/session-snapshot-persist.js +76 -0
  60. package/dist/hooks/adaptive-threshold.d.ts +27 -0
  61. package/dist/hooks/adaptive-threshold.js +46 -0
  62. package/dist/hooks/format-deny-message.d.ts +21 -0
  63. package/dist/hooks/format-deny-message.js +147 -0
  64. package/dist/hooks/installer.d.ts +7 -1
  65. package/dist/hooks/installer.js +175 -55
  66. package/dist/hooks/path-safety.d.ts +16 -0
  67. package/dist/hooks/path-safety.js +34 -0
  68. package/dist/hooks/post-bash.d.ts +46 -0
  69. package/dist/hooks/post-bash.js +77 -0
  70. package/dist/hooks/session-start.d.ts +45 -0
  71. package/dist/hooks/session-start.js +179 -0
  72. package/dist/hooks/summary-ast-index.d.ts +28 -0
  73. package/dist/hooks/summary-ast-index.js +122 -0
  74. package/dist/hooks/summary-head-tail.d.ts +15 -0
  75. package/dist/hooks/summary-head-tail.js +78 -0
  76. package/dist/hooks/summary-pipeline.d.ts +35 -0
  77. package/dist/hooks/summary-pipeline.js +63 -0
  78. package/dist/hooks/summary-regex.d.ts +14 -0
  79. package/dist/hooks/summary-regex.js +130 -0
  80. package/dist/hooks/summary-types.d.ts +29 -0
  81. package/dist/hooks/summary-types.js +9 -0
  82. package/dist/index.d.ts +15 -3
  83. package/dist/index.js +508 -131
  84. package/dist/integration/context-mode-detector.d.ts +7 -1
  85. package/dist/integration/context-mode-detector.js +51 -15
  86. package/dist/server/tool-definitions.d.ts +149 -0
  87. package/dist/server/tool-definitions.js +424 -202
  88. package/dist/server.d.ts +1 -1
  89. package/dist/server.js +456 -179
  90. package/dist/templates/agent-builder.d.ts +49 -0
  91. package/dist/templates/agent-builder.js +104 -0
  92. package/dist/types.d.ts +38 -4
  93. package/package.json +89 -87
  94. package/skills/stats/SKILL.md +13 -2
@@ -27,12 +27,16 @@ export declare class AstIndexClient {
27
27
  symbol(name: string): Promise<AstIndexSymbolDetail | null>;
28
28
  search(query: string, options?: {
29
29
  inFile?: string;
30
+ type?: string;
30
31
  maxResults?: number;
31
32
  fuzzy?: boolean;
32
33
  }): Promise<AstIndexSearchResult[]>;
33
34
  usages(symbolName: string): Promise<AstIndexUsageResult[]>;
34
35
  implementations(name: string): Promise<AstIndexImplementation[]>;
35
- hierarchy(name: string): Promise<AstIndexHierarchyNode | null>;
36
+ hierarchy(name: string, options?: {
37
+ inFile?: string;
38
+ module?: string;
39
+ }): Promise<AstIndexHierarchyNode | null>;
36
40
  stats(): Promise<string | null>;
37
41
  listFiles(): Promise<string[]>;
38
42
  refs(symbolName: string, limit?: number): Promise<AstIndexRefsResponse>;
@@ -223,6 +223,8 @@ export class AstIndexClient {
223
223
  const args = ['search', query, '--format', 'json'];
224
224
  if (options?.inFile)
225
225
  args.push('--in-file', options.inFile);
226
+ if (options?.type)
227
+ args.push('--type', options.type);
226
228
  if (options?.maxResults)
227
229
  args.push('--limit', String(options.maxResults));
228
230
  if (options?.fuzzy)
@@ -295,10 +297,15 @@ export class AstIndexClient {
295
297
  return [];
296
298
  }
297
299
  }
298
- async hierarchy(name) {
300
+ async hierarchy(name, options) {
299
301
  await this.ensureIndex();
300
302
  try {
301
- const result = await this.exec(['hierarchy', name, '--format', 'json']);
303
+ const args = ['hierarchy', name, '--format', 'json'];
304
+ if (options?.inFile)
305
+ args.push('--in-file', options.inFile);
306
+ if (options?.module)
307
+ args.push('--module', options.module);
308
+ const result = await this.exec(args);
302
309
  try {
303
310
  return JSON.parse(result);
304
311
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Agent-frontmatter helpers (subtask 3.1).
3
+ *
4
+ * Parses and writes YAML-style frontmatter in Claude Code agent .md files.
5
+ * Handles the three tools-field forms:
6
+ * - wildcard : "*" | "All tools"
7
+ * - exclusion : "All tools [except X, Y]"
8
+ * - explicit : "Read, Edit, Bash" | string[] (YAML list)
9
+ */
10
+ export type ToolsWildcard = {
11
+ kind: "wildcard";
12
+ };
13
+ export type ToolsExclusion = {
14
+ kind: "exclusion";
15
+ excluded: string[];
16
+ };
17
+ export type ToolsExplicit = {
18
+ kind: "explicit";
19
+ tools: string[];
20
+ };
21
+ export type ParsedTools = ToolsWildcard | ToolsExclusion | ToolsExplicit;
22
+ export interface FrontmatterResult {
23
+ /** Parsed YAML fields. Values may be strings, arrays, or nested objects. */
24
+ meta: Record<string, any>;
25
+ /** Everything after the closing --- delimiter (may be empty string). */
26
+ body: string;
27
+ }
28
+ /**
29
+ * Parse YAML-style frontmatter from an agent markdown file.
30
+ *
31
+ * Handles:
32
+ * - Simple key: value pairs
33
+ * - YAML list items (- value)
34
+ * - Nested blocks (token_pilot: / sub-key: value)
35
+ * - CRLF line endings
36
+ */
37
+ export declare function parseFrontmatter(md: string): FrontmatterResult;
38
+ /**
39
+ * Serialize meta + body back to a markdown string with YAML frontmatter.
40
+ */
41
+ export declare function writeFrontmatter({ meta, body }: FrontmatterResult): string;
42
+ /**
43
+ * Parse the tools field from an agent frontmatter into one of three forms.
44
+ *
45
+ * @param raw - Raw value from parsed frontmatter (string, string[], or undefined)
46
+ */
47
+ export declare function parseToolsField(raw: string | string[] | undefined): ParsedTools;
48
+ //# sourceMappingURL=agent-frontmatter.d.ts.map
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Agent-frontmatter helpers (subtask 3.1).
3
+ *
4
+ * Parses and writes YAML-style frontmatter in Claude Code agent .md files.
5
+ * Handles the three tools-field forms:
6
+ * - wildcard : "*" | "All tools"
7
+ * - exclusion : "All tools [except X, Y]"
8
+ * - explicit : "Read, Edit, Bash" | string[] (YAML list)
9
+ */
10
+ // ─── parseFrontmatter ─────────────────────────────────────────────────────────
11
+ /**
12
+ * Parse YAML-style frontmatter from an agent markdown file.
13
+ *
14
+ * Handles:
15
+ * - Simple key: value pairs
16
+ * - YAML list items (- value)
17
+ * - Nested blocks (token_pilot: / sub-key: value)
18
+ * - CRLF line endings
19
+ */
20
+ export function parseFrontmatter(md) {
21
+ const match = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
22
+ if (!match) {
23
+ return { meta: {}, body: md };
24
+ }
25
+ const yamlText = match[1];
26
+ const body = match[2] ?? "";
27
+ const meta = parseSimpleYaml(yamlText);
28
+ return { meta, body };
29
+ }
30
+ /**
31
+ * Parse a subset of YAML sufficient for agent frontmatter.
32
+ * Supports: scalar values, inline lists, block lists, nested maps.
33
+ */
34
+ function parseSimpleYaml(text) {
35
+ const result = {};
36
+ const lines = text.split(/\r?\n/);
37
+ let i = 0;
38
+ while (i < lines.length) {
39
+ const line = lines[i];
40
+ // Skip empty lines
41
+ if (!line.trim()) {
42
+ i++;
43
+ continue;
44
+ }
45
+ // Top-level key: value
46
+ const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
47
+ if (!kv) {
48
+ i++;
49
+ continue;
50
+ }
51
+ const key = kv[1];
52
+ const rawVal = kv[2].trim();
53
+ // Empty value → could be a nested block or inline list
54
+ if (rawVal === "") {
55
+ // Look ahead for indented lines (nested block or list)
56
+ const nested = [];
57
+ i++;
58
+ while (i < lines.length &&
59
+ (lines[i].startsWith(" ") || lines[i].startsWith("\t"))) {
60
+ nested.push(lines[i]);
61
+ i++;
62
+ }
63
+ if (nested.length === 0) {
64
+ result[key] = "";
65
+ continue;
66
+ }
67
+ // Check if it's a list (items start with " - ")
68
+ if (nested[0].trim().startsWith("- ")) {
69
+ result[key] = nested.map((l) => l.trim().replace(/^-\s+/, ""));
70
+ }
71
+ else {
72
+ // Nested map — dedent and recurse
73
+ const dedented = nested.map((l) => l.replace(/^ /, "")).join("\n");
74
+ result[key] = parseSimpleYaml(dedented);
75
+ }
76
+ continue;
77
+ }
78
+ // Inline YAML list: [a, b, c]
79
+ if (rawVal.startsWith("[") && rawVal.endsWith("]")) {
80
+ result[key] = rawVal
81
+ .slice(1, -1)
82
+ .split(",")
83
+ .map((s) => s.trim())
84
+ .filter(Boolean);
85
+ i++;
86
+ continue;
87
+ }
88
+ // Boolean values
89
+ if (rawVal === "true") {
90
+ result[key] = true;
91
+ i++;
92
+ continue;
93
+ }
94
+ if (rawVal === "false") {
95
+ result[key] = false;
96
+ i++;
97
+ continue;
98
+ }
99
+ // Strip surrounding quotes
100
+ if ((rawVal.startsWith("'") && rawVal.endsWith("'")) ||
101
+ (rawVal.startsWith('"') && rawVal.endsWith('"'))) {
102
+ result[key] = rawVal.slice(1, -1);
103
+ i++;
104
+ continue;
105
+ }
106
+ result[key] = rawVal;
107
+ i++;
108
+ }
109
+ return result;
110
+ }
111
+ // ─── writeFrontmatter ─────────────────────────────────────────────────────────
112
+ /**
113
+ * Serialize meta + body back to a markdown string with YAML frontmatter.
114
+ */
115
+ export function writeFrontmatter({ meta, body }) {
116
+ const yaml = serializeYaml(meta);
117
+ return `---\n${yaml}---\n${body}`;
118
+ }
119
+ function serializeYaml(obj, indent = "") {
120
+ let out = "";
121
+ for (const [key, val] of Object.entries(obj)) {
122
+ if (val === null || val === undefined)
123
+ continue;
124
+ if (Array.isArray(val)) {
125
+ out += `${indent}${key}:\n`;
126
+ for (const item of val) {
127
+ out += `${indent} - ${item}\n`;
128
+ }
129
+ }
130
+ else if (typeof val === "object") {
131
+ out += `${indent}${key}:\n`;
132
+ out += serializeYaml(val, indent + " ");
133
+ }
134
+ else if (typeof val === "boolean") {
135
+ out += `${indent}${key}: ${val}\n`;
136
+ }
137
+ else {
138
+ // Quote strings containing special YAML chars
139
+ const s = String(val);
140
+ const needsQuote = /[:#\[\]{}&*!,|>'"%@`]/.test(s) || s.trim() !== s;
141
+ out += `${indent}${key}: ${needsQuote ? `"${s.replace(/"/g, '\\"')}"` : s}\n`;
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+ // ─── parseToolsField ─────────────────────────────────────────────────────────
147
+ /**
148
+ * Parse the tools field from an agent frontmatter into one of three forms.
149
+ *
150
+ * @param raw - Raw value from parsed frontmatter (string, string[], or undefined)
151
+ */
152
+ export function parseToolsField(raw) {
153
+ // Array form — already a YAML list
154
+ if (Array.isArray(raw)) {
155
+ return {
156
+ kind: "explicit",
157
+ tools: raw.map((s) => s.trim()).filter(Boolean),
158
+ };
159
+ }
160
+ if (!raw) {
161
+ return { kind: "explicit", tools: [] };
162
+ }
163
+ const s = raw.trim();
164
+ // Strip surrounding quotes
165
+ const unquoted = (s.startsWith("'") && s.endsWith("'")) ||
166
+ (s.startsWith('"') && s.endsWith('"'))
167
+ ? s.slice(1, -1).trim()
168
+ : s;
169
+ // Wildcard forms
170
+ if (unquoted === "*" || unquoted === "All tools") {
171
+ return { kind: "wildcard" };
172
+ }
173
+ // Exclusion form: "All tools [except X, Y]" or "All tools except X, Y"
174
+ const exclusionMatch = unquoted.match(/^All tools\s+(?:\[except\s+|except\s+)(.*?)\]?$/i);
175
+ if (exclusionMatch) {
176
+ const excluded = exclusionMatch[1]
177
+ .split(",")
178
+ .map((s) => s.trim())
179
+ .filter(Boolean);
180
+ return { kind: "exclusion", excluded };
181
+ }
182
+ // Explicit comma-separated list
183
+ const tools = unquoted
184
+ .split(",")
185
+ .map((s) => s.trim())
186
+ .filter(Boolean);
187
+ return { kind: "explicit", tools };
188
+ }
189
+ //# sourceMappingURL=agent-frontmatter.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * bless-agents CLI (subtasks 3.4 + 3.5).
3
+ *
4
+ * Given a Category-C ScannedAgent:
5
+ * - Reads upstream file
6
+ * - Extends tools list with the 6 mcp__token-pilot__* tool names
7
+ * - Adds token_pilot frontmatter block with blessed marker
8
+ * - Copies upstream body verbatim
9
+ * - Writes atomically to ./.claude/agents/<name>.md
10
+ *
11
+ * Never overwrites a file without blessed:true unless --force.
12
+ * Never overwrites a user prior customisation (file without marker).
13
+ */
14
+ import type { ScannedAgent } from "./scan-agents.js";
15
+ export declare const TP_MCP_TOOLS: string[];
16
+ export interface BlessOptions {
17
+ projectRoot: string;
18
+ tokenPilotVersion: string;
19
+ force: boolean;
20
+ dryRun: boolean;
21
+ }
22
+ export type BlessResult = {
23
+ kind: "blessed";
24
+ destPath: string;
25
+ } | {
26
+ kind: "skipped";
27
+ reason: string;
28
+ } | {
29
+ kind: "dry-run";
30
+ destPath: string;
31
+ } | {
32
+ kind: "error";
33
+ reason: string;
34
+ };
35
+ export interface BlessSummary {
36
+ blessed: number;
37
+ skipped: number;
38
+ errors: number;
39
+ }
40
+ /**
41
+ * Bless a single Category-C agent.
42
+ * Reads upstream file, builds new frontmatter + body, writes atomically.
43
+ */
44
+ export declare function blessAgent(agent: ScannedAgent, opts: BlessOptions): Promise<BlessResult>;
45
+ /**
46
+ * Bless a list of agents and print a summary to stderr.
47
+ */
48
+ export declare function blessAll(agents: ScannedAgent[], opts: BlessOptions): Promise<BlessSummary>;
49
+ export type PromptChoice = "all" | "interactive" | "no";
50
+ /**
51
+ * Present the classified list and ask the user what to do.
52
+ * Returns the choice or throws if stdin is not a TTY without --auto.
53
+ */
54
+ export declare function promptBlessChoice(candidates: ScannedAgent[]): Promise<PromptChoice>;
55
+ /**
56
+ * Interactive mode: ask per-agent.
57
+ * Returns list of agents the user confirmed.
58
+ */
59
+ export declare function promptInteractive(candidates: ScannedAgent[]): Promise<ScannedAgent[]>;
60
+ /**
61
+ * Main entry for `token-pilot bless-agents` command.
62
+ * Called from src/index.ts — delegates scanning + blessing.
63
+ */
64
+ export declare function handleBlessAgents(argv: string[]): Promise<void>;
65
+ //# sourceMappingURL=bless-agents.d.ts.map
@@ -0,0 +1,307 @@
1
+ /**
2
+ * bless-agents CLI (subtasks 3.4 + 3.5).
3
+ *
4
+ * Given a Category-C ScannedAgent:
5
+ * - Reads upstream file
6
+ * - Extends tools list with the 6 mcp__token-pilot__* tool names
7
+ * - Adds token_pilot frontmatter block with blessed marker
8
+ * - Copies upstream body verbatim
9
+ * - Writes atomically to ./.claude/agents/<name>.md
10
+ *
11
+ * Never overwrites a file without blessed:true unless --force.
12
+ * Never overwrites a user prior customisation (file without marker).
13
+ */
14
+ import { readFile, writeFile, mkdir, rename, access } from "node:fs/promises";
15
+ import { join, dirname } from "node:path";
16
+ import { createHash } from "node:crypto";
17
+ import { createInterface } from "node:readline";
18
+ import { parseFrontmatter, writeFrontmatter } from "./agent-frontmatter.js";
19
+ // ─── Constants ────────────────────────────────────────────────────────────────
20
+ export const TP_MCP_TOOLS = [
21
+ "mcp__token-pilot__smart_read",
22
+ "mcp__token-pilot__read_symbol",
23
+ "mcp__token-pilot__read_for_edit",
24
+ "mcp__token-pilot__outline",
25
+ "mcp__token-pilot__find_usages",
26
+ "mcp__token-pilot__explore_area",
27
+ ];
28
+ // ─── Core: blessAgent ─────────────────────────────────────────────────────────
29
+ /**
30
+ * Bless a single Category-C agent.
31
+ * Reads upstream file, builds new frontmatter + body, writes atomically.
32
+ */
33
+ export async function blessAgent(agent, opts) {
34
+ const destPath = join(opts.projectRoot, ".claude", "agents", `${agent.name}.md`);
35
+ // ── Check destination ──────────────────────────────────────────────────────
36
+ let destExists = false;
37
+ try {
38
+ await access(destPath);
39
+ destExists = true;
40
+ }
41
+ catch {
42
+ destExists = false;
43
+ }
44
+ if (destExists) {
45
+ let destContent;
46
+ try {
47
+ destContent = await readFile(destPath, "utf-8");
48
+ }
49
+ catch (err) {
50
+ return {
51
+ kind: "error",
52
+ reason: `Cannot read existing destination: ${err instanceof Error ? err.message : err}`,
53
+ };
54
+ }
55
+ const { meta: destMeta } = parseFrontmatter(destContent);
56
+ const isOurBlessed = destMeta.token_pilot !== null &&
57
+ typeof destMeta.token_pilot === "object" &&
58
+ destMeta.token_pilot.blessed === true;
59
+ if (isOurBlessed && !opts.force) {
60
+ return {
61
+ kind: "skipped",
62
+ reason: `already blessed — use --force to re-bless`,
63
+ };
64
+ }
65
+ if (!isOurBlessed) {
66
+ // Exists without our marker → user's prior customisation → always skip
67
+ return {
68
+ kind: "skipped",
69
+ reason: `prior customisation exists without blessed marker — skipping to respect user override`,
70
+ };
71
+ }
72
+ // isOurBlessed && force → fall through to overwrite
73
+ }
74
+ // ── Read upstream ──────────────────────────────────────────────────────────
75
+ let upstreamContent;
76
+ try {
77
+ upstreamContent = await readFile(agent.path, "utf-8");
78
+ }
79
+ catch (err) {
80
+ return {
81
+ kind: "error",
82
+ reason: `Cannot read upstream file ${agent.path}: ${err instanceof Error ? err.message : err}`,
83
+ };
84
+ }
85
+ const { meta: upstreamMeta, body: upstreamBody } = parseFrontmatter(upstreamContent);
86
+ // ── Build new tools list ───────────────────────────────────────────────────
87
+ // Start from the upstream tools (may be string or array), add TP tools
88
+ let existingTools = [];
89
+ if (Array.isArray(upstreamMeta.tools)) {
90
+ existingTools = upstreamMeta.tools.map((t) => String(t).trim());
91
+ }
92
+ else if (typeof upstreamMeta.tools === "string") {
93
+ existingTools = upstreamMeta.tools
94
+ .split(",")
95
+ .map((t) => t.trim())
96
+ .filter(Boolean);
97
+ }
98
+ // De-duplicate: add only missing TP tools
99
+ const newTools = [...existingTools];
100
+ for (const t of TP_MCP_TOOLS) {
101
+ if (!newTools.includes(t)) {
102
+ newTools.push(t);
103
+ }
104
+ }
105
+ // ── Compute upstream hash ─────────────────────────────────────────────────
106
+ const upstreamHash = createHash("sha256")
107
+ .update(upstreamContent)
108
+ .digest("hex");
109
+ // ── Build new meta ─────────────────────────────────────────────────────────
110
+ const newMeta = {
111
+ name: upstreamMeta.name ?? agent.name,
112
+ description: upstreamMeta.description ?? agent.description,
113
+ tools: newTools,
114
+ token_pilot: {
115
+ blessed: true,
116
+ upstream: agent.scope,
117
+ blessed_at: new Date().toISOString(),
118
+ token_pilot_version: opts.tokenPilotVersion,
119
+ upstream_hash: upstreamHash,
120
+ },
121
+ };
122
+ // ── Dry run ────────────────────────────────────────────────────────────────
123
+ if (opts.dryRun) {
124
+ return { kind: "dry-run", destPath };
125
+ }
126
+ // ── Write atomically ───────────────────────────────────────────────────────
127
+ const newContent = writeFrontmatter({ meta: newMeta, body: upstreamBody });
128
+ const destDir = dirname(destPath);
129
+ await mkdir(destDir, { recursive: true });
130
+ const tmpPath = `${destPath}.tmp-${Math.random().toString(36).slice(2)}`;
131
+ try {
132
+ await writeFile(tmpPath, newContent, "utf-8");
133
+ await rename(tmpPath, destPath);
134
+ }
135
+ catch (err) {
136
+ // Clean up tmp on failure
137
+ try {
138
+ await writeFile(tmpPath, ""); // truncate so rename can't partially exist
139
+ }
140
+ catch {
141
+ // ignore
142
+ }
143
+ return {
144
+ kind: "error",
145
+ reason: `Write failed: ${err instanceof Error ? err.message : err}`,
146
+ };
147
+ }
148
+ return { kind: "blessed", destPath };
149
+ }
150
+ // ─── blessAll ─────────────────────────────────────────────────────────────────
151
+ /**
152
+ * Bless a list of agents and print a summary to stderr.
153
+ */
154
+ export async function blessAll(agents, opts) {
155
+ const summary = { blessed: 0, skipped: 0, errors: 0 };
156
+ for (const agent of agents) {
157
+ const result = await blessAgent(agent, opts);
158
+ switch (result.kind) {
159
+ case "blessed":
160
+ summary.blessed++;
161
+ break;
162
+ case "skipped":
163
+ case "dry-run":
164
+ summary.skipped++;
165
+ break;
166
+ case "error":
167
+ summary.errors++;
168
+ process.stderr.write(`token-pilot bless-agents: error on ${agent.name}: ${result.reason}\n`);
169
+ break;
170
+ }
171
+ }
172
+ if (!opts.dryRun) {
173
+ process.stderr.write(`Blessed ${summary.blessed} agent${summary.blessed === 1 ? "" : "s"} to .claude/agents/. Start a new Claude Code session to pick them up.\n`);
174
+ }
175
+ return summary;
176
+ }
177
+ /**
178
+ * Present the classified list and ask the user what to do.
179
+ * Returns the choice or throws if stdin is not a TTY without --auto.
180
+ */
181
+ export async function promptBlessChoice(candidates) {
182
+ if (!process.stdin.isTTY) {
183
+ process.stderr.write("Run with --auto to install non-interactively, or from a TTY.\n");
184
+ process.exit(1);
185
+ }
186
+ process.stderr.write(`\nFound ${candidates.length} agent(s) to bless:\n`);
187
+ for (const a of candidates) {
188
+ process.stderr.write(` - ${a.name} (${a.scope})\n`);
189
+ }
190
+ process.stderr.write("\nCreate project-level overrides with MCP tools added?\n");
191
+ process.stderr.write(" [a] Yes, all\n [i] Interactive (ask per agent)\n [n] No, cancel\n\n> ");
192
+ return new Promise((resolve) => {
193
+ const rl = createInterface({
194
+ input: process.stdin,
195
+ output: process.stderr,
196
+ });
197
+ rl.question("", (answer) => {
198
+ rl.close();
199
+ const a = answer.trim().toLowerCase();
200
+ if (a === "a" || a === "all")
201
+ resolve("all");
202
+ else if (a === "i" || a === "interactive")
203
+ resolve("interactive");
204
+ else
205
+ resolve("no");
206
+ });
207
+ });
208
+ }
209
+ /**
210
+ * Interactive mode: ask per-agent.
211
+ * Returns list of agents the user confirmed.
212
+ */
213
+ export async function promptInteractive(candidates) {
214
+ const chosen = [];
215
+ for (const agent of candidates) {
216
+ const answer = await new Promise((resolve) => {
217
+ process.stderr.write(`Bless ${agent.name} (${agent.scope})? [y/n] `);
218
+ const rl = createInterface({
219
+ input: process.stdin,
220
+ output: process.stderr,
221
+ });
222
+ rl.question("", (a) => {
223
+ rl.close();
224
+ resolve(a.trim().toLowerCase());
225
+ });
226
+ });
227
+ if (answer === "y" || answer === "yes") {
228
+ chosen.push(agent);
229
+ }
230
+ }
231
+ return chosen;
232
+ }
233
+ // ─── CLI entry ────────────────────────────────────────────────────────────────
234
+ /**
235
+ * Main entry for `token-pilot bless-agents` command.
236
+ * Called from src/index.ts — delegates scanning + blessing.
237
+ */
238
+ export async function handleBlessAgents(argv) {
239
+ const { scanAgents, classifyAgent } = await import("./scan-agents.js");
240
+ const { homedir } = await import("node:os");
241
+ const { existsSync } = await import("node:fs");
242
+ const auto = argv.includes("--auto");
243
+ const force = argv.includes("--force");
244
+ const dryRun = argv.includes("--dry-run");
245
+ const homeDir = homedir();
246
+ const projectRoot = process.cwd();
247
+ // Derive token-pilot version from package.json
248
+ let tpVersion = "0.0.0";
249
+ try {
250
+ const pkgPath = new URL("../../package.json", import.meta.url);
251
+ const pkg = JSON.parse(await readFile(pkgPath.pathname, "utf-8"));
252
+ tpVersion = pkg.version;
253
+ }
254
+ catch {
255
+ // fallback
256
+ }
257
+ // Build plugin cache globs
258
+ const pluginCacheBase = join(homeDir, ".claude", "plugins", "cache");
259
+ const pluginCacheGlob = existsSync(pluginCacheBase)
260
+ ? [`${pluginCacheBase}/**/agents/*.md`]
261
+ : [];
262
+ let agents;
263
+ try {
264
+ agents = await scanAgents({ projectRoot, homeDir, pluginCacheGlob });
265
+ }
266
+ catch (err) {
267
+ process.stderr.write(`token-pilot bless-agents: scan failed: ${err instanceof Error ? err.message : err}\n`);
268
+ process.exit(1);
269
+ }
270
+ // Filter to Category-C candidates only
271
+ const candidates = agents.filter((a) => classifyAgent(a) === "C");
272
+ if (candidates.length === 0) {
273
+ process.stderr.write("No Category-C agents found. All agents already have token-pilot MCP access or no agents are installed.\n");
274
+ process.exit(0);
275
+ }
276
+ const opts = {
277
+ projectRoot,
278
+ tokenPilotVersion: tpVersion,
279
+ force,
280
+ dryRun,
281
+ };
282
+ let toProcess = candidates;
283
+ if (!auto) {
284
+ // Interactive or TTY check
285
+ if (!process.stdin.isTTY) {
286
+ process.stderr.write("Run with --auto to install non-interactively, or from a TTY.\n");
287
+ process.exit(1);
288
+ }
289
+ const choice = await promptBlessChoice(candidates);
290
+ if (choice === "no") {
291
+ process.stderr.write("Cancelled.\n");
292
+ process.exit(0);
293
+ }
294
+ if (choice === "interactive") {
295
+ toProcess = await promptInteractive(candidates);
296
+ if (toProcess.length === 0) {
297
+ process.stderr.write("No agents selected.\n");
298
+ process.exit(0);
299
+ }
300
+ }
301
+ }
302
+ const summary = await blessAll(toProcess, opts);
303
+ if (summary.errors > 0) {
304
+ process.exit(1);
305
+ }
306
+ }
307
+ //# sourceMappingURL=bless-agents.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * TP-rtg (part 1) — `.claudeignore` generator.
3
+ *
4
+ * The `.claudeignore` file is a community convention (mirrors `.gitignore`):
5
+ * Claude Code and other tools skip listed paths when building context.
6
+ * Populating it with sensible defaults gives a one-time, permanent drop in
7
+ * per-message token cost (node_modules, dist, lockfiles etc.).
8
+ *
9
+ * Non-destructive: we never overwrite a user-owned file. The file we
10
+ * generate carries a magic comment so the tool can recognise its own
11
+ * past output on re-run and refresh the defaults in place.
12
+ */
13
+ export declare const CLAUDEIGNORE_MANAGED_MARKER = "# token-pilot managed defaults (safe to edit; marker keeps this file auto-refreshable)";
14
+ export declare const DEFAULT_IGNORE_ENTRIES: readonly string[];
15
+ export type ClaudeIgnoreStatus = {
16
+ kind: "absent";
17
+ } | {
18
+ kind: "managed";
19
+ } | {
20
+ kind: "user-owned";
21
+ };
22
+ /**
23
+ * Determine the current state of `<projectRoot>/.claudeignore`:
24
+ * absent, written by us (managed), or authored by the user (user-owned).
25
+ */
26
+ export declare function claudeIgnoreStatus(projectRoot: string): Promise<ClaudeIgnoreStatus>;
27
+ /**
28
+ * Write (or refresh) the default `.claudeignore`. Returns true when we
29
+ * actually touched the file, false when we refused to avoid clobbering
30
+ * user content.
31
+ */
32
+ export declare function writeDefaultClaudeIgnore(projectRoot: string): Promise<boolean>;
33
+ //# sourceMappingURL=claudeignore.d.ts.map