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.
- package/.claude-plugin/hooks/hooks.json +21 -0
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +129 -0
- package/README.md +172 -315
- package/dist/agents/tp-commit-writer.md +41 -0
- package/dist/agents/tp-dead-code-finder.md +43 -0
- package/dist/agents/tp-debugger.md +45 -0
- package/dist/agents/tp-impact-analyzer.md +44 -0
- package/dist/agents/tp-migration-scout.md +43 -0
- package/dist/agents/tp-onboard.md +40 -0
- package/dist/agents/tp-pr-reviewer.md +41 -0
- package/dist/agents/tp-refactor-planner.md +42 -0
- package/dist/agents/tp-run.md +48 -0
- package/dist/agents/tp-test-triage.md +40 -0
- package/dist/agents/tp-test-writer.md +46 -0
- package/dist/cli/agent-frontmatter.d.ts +48 -0
- package/dist/cli/agent-frontmatter.js +189 -0
- package/dist/cli/bless-agents.d.ts +65 -0
- package/dist/cli/bless-agents.js +307 -0
- package/dist/cli/claudeignore.d.ts +33 -0
- package/dist/cli/claudeignore.js +88 -0
- package/dist/cli/claudemd-hygiene.d.ts +26 -0
- package/dist/cli/claudemd-hygiene.js +43 -0
- package/dist/cli/doctor-drift.d.ts +31 -0
- package/dist/cli/doctor-drift.js +130 -0
- package/dist/cli/doctor-env-check.d.ts +25 -0
- package/dist/cli/doctor-env-check.js +91 -0
- package/dist/cli/install-agents.d.ts +108 -0
- package/dist/cli/install-agents.js +402 -0
- package/dist/cli/save-doc.d.ts +42 -0
- package/dist/cli/save-doc.js +145 -0
- package/dist/cli/scan-agents.d.ts +46 -0
- package/dist/cli/scan-agents.js +227 -0
- package/dist/cli/stats.d.ts +36 -0
- package/dist/cli/stats.js +131 -0
- package/dist/cli/unbless-agents.d.ts +33 -0
- package/dist/cli/unbless-agents.js +85 -0
- package/dist/cli/uninstall-agents.d.ts +36 -0
- package/dist/cli/uninstall-agents.js +117 -0
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +14 -8
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +105 -11
- package/dist/core/context-registry.d.ts +16 -1
- package/dist/core/context-registry.js +60 -28
- package/dist/core/event-log.d.ts +79 -0
- package/dist/core/event-log.js +190 -0
- package/dist/core/session-registry.d.ts +43 -0
- package/dist/core/session-registry.js +113 -0
- package/dist/core/session-savings.d.ts +19 -0
- package/dist/core/session-savings.js +60 -0
- package/dist/handlers/session-budget.d.ts +32 -0
- package/dist/handlers/session-budget.js +61 -0
- package/dist/handlers/session-snapshot-persist.d.ts +22 -0
- package/dist/handlers/session-snapshot-persist.js +76 -0
- package/dist/hooks/adaptive-threshold.d.ts +27 -0
- package/dist/hooks/adaptive-threshold.js +46 -0
- package/dist/hooks/format-deny-message.d.ts +21 -0
- package/dist/hooks/format-deny-message.js +147 -0
- package/dist/hooks/installer.js +121 -31
- package/dist/hooks/path-safety.d.ts +16 -0
- package/dist/hooks/path-safety.js +34 -0
- package/dist/hooks/post-bash.d.ts +46 -0
- package/dist/hooks/post-bash.js +77 -0
- package/dist/hooks/session-start.d.ts +45 -0
- package/dist/hooks/session-start.js +179 -0
- package/dist/hooks/summary-ast-index.d.ts +28 -0
- package/dist/hooks/summary-ast-index.js +122 -0
- package/dist/hooks/summary-head-tail.d.ts +15 -0
- package/dist/hooks/summary-head-tail.js +78 -0
- package/dist/hooks/summary-pipeline.d.ts +35 -0
- package/dist/hooks/summary-pipeline.js +63 -0
- package/dist/hooks/summary-regex.d.ts +14 -0
- package/dist/hooks/summary-regex.js +130 -0
- package/dist/hooks/summary-types.d.ts +29 -0
- package/dist/hooks/summary-types.js +9 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +509 -149
- package/dist/integration/context-mode-detector.d.ts +7 -1
- package/dist/integration/context-mode-detector.js +51 -15
- package/dist/server/tool-definitions.d.ts +149 -0
- package/dist/server/tool-definitions.js +424 -202
- package/dist/server.d.ts +1 -1
- package/dist/server.js +456 -179
- package/dist/templates/agent-builder.d.ts +49 -0
- package/dist/templates/agent-builder.js +104 -0
- package/dist/types.d.ts +38 -4
- package/package.json +4 -2
- package/skills/stats/SKILL.md +13 -2
|
@@ -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
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
import { readFile, writeFile, access } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
export const CLAUDEIGNORE_MANAGED_MARKER = "# token-pilot managed defaults (safe to edit; marker keeps this file auto-refreshable)";
|
|
16
|
+
export const DEFAULT_IGNORE_ENTRIES = [
|
|
17
|
+
"node_modules/",
|
|
18
|
+
"dist/",
|
|
19
|
+
"build/",
|
|
20
|
+
".next/",
|
|
21
|
+
".nuxt/",
|
|
22
|
+
".turbo/",
|
|
23
|
+
"__pycache__/",
|
|
24
|
+
".venv/",
|
|
25
|
+
"venv/",
|
|
26
|
+
"target/",
|
|
27
|
+
"coverage/",
|
|
28
|
+
".coverage/",
|
|
29
|
+
"*.min.js",
|
|
30
|
+
"*.min.css",
|
|
31
|
+
"*.map",
|
|
32
|
+
"package-lock.json",
|
|
33
|
+
"yarn.lock",
|
|
34
|
+
"pnpm-lock.yaml",
|
|
35
|
+
"Cargo.lock",
|
|
36
|
+
"Pipfile.lock",
|
|
37
|
+
"poetry.lock",
|
|
38
|
+
"composer.lock",
|
|
39
|
+
];
|
|
40
|
+
function pathFor(projectRoot) {
|
|
41
|
+
return join(projectRoot, ".claudeignore");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Determine the current state of `<projectRoot>/.claudeignore`:
|
|
45
|
+
* absent, written by us (managed), or authored by the user (user-owned).
|
|
46
|
+
*/
|
|
47
|
+
export async function claudeIgnoreStatus(projectRoot) {
|
|
48
|
+
const p = pathFor(projectRoot);
|
|
49
|
+
try {
|
|
50
|
+
await access(p);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { kind: "absent" };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const content = await readFile(p, "utf-8");
|
|
57
|
+
if (content.includes(CLAUDEIGNORE_MANAGED_MARKER))
|
|
58
|
+
return { kind: "managed" };
|
|
59
|
+
return { kind: "user-owned" };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { kind: "user-owned" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write (or refresh) the default `.claudeignore`. Returns true when we
|
|
67
|
+
* actually touched the file, false when we refused to avoid clobbering
|
|
68
|
+
* user content.
|
|
69
|
+
*/
|
|
70
|
+
export async function writeDefaultClaudeIgnore(projectRoot) {
|
|
71
|
+
const status = await claudeIgnoreStatus(projectRoot);
|
|
72
|
+
if (status.kind === "user-owned")
|
|
73
|
+
return false;
|
|
74
|
+
const body = `${CLAUDEIGNORE_MANAGED_MARKER}\n` +
|
|
75
|
+
`# Paths Claude Code and friendly tools will skip when building context.\n` +
|
|
76
|
+
`# Remove or edit entries; the marker line above keeps this file refreshable\n` +
|
|
77
|
+
`# by \`token-pilot init\` / \`token-pilot doctor\`.\n\n` +
|
|
78
|
+
DEFAULT_IGNORE_ENTRIES.join("\n") +
|
|
79
|
+
"\n";
|
|
80
|
+
try {
|
|
81
|
+
await writeFile(pathFor(projectRoot), body);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=claudeignore.js.map
|