token-pilot 0.19.2 → 0.23.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/.claude-plugin/hooks/hooks.json +30 -0
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +165 -0
- package/README.md +194 -313
- package/dist/agents/tp-audit-scanner.md +49 -0
- 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-history-explorer.md +43 -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-session-restorer.md +47 -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/typo-guard.d.ts +27 -0
- package/dist/cli/typo-guard.js +119 -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 +130 -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/post-task.d.ts +67 -0
- package/dist/hooks/post-task.js +136 -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 +538 -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
package/dist/index.js
CHANGED
|
@@ -1,65 +1,230 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { StdioServerTransport } from
|
|
3
|
-
import { readFileSync, realpathSync, appendFileSync, mkdirSync } from
|
|
4
|
-
import { join } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { readFileSync, realpathSync, appendFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { createServer } from "./server.js";
|
|
10
|
+
import { installHook, uninstallHook } from "./hooks/installer.js";
|
|
11
|
+
import { findBinary, installBinary, checkBinaryUpdate, isNewerVersion, } from "./ast-index/binary-manager.js";
|
|
12
|
+
import { loadConfig } from "./config/loader.js";
|
|
13
|
+
import { isDangerousRoot } from "./core/validation.js";
|
|
14
|
+
import { runSummaryPipeline } from "./hooks/summary-pipeline.js";
|
|
15
|
+
import { formatDenyMessage } from "./hooks/format-deny-message.js";
|
|
16
|
+
import { isPathWithinProject } from "./hooks/path-safety.js";
|
|
17
|
+
import { handleSessionStart } from "./hooks/session-start.js";
|
|
18
|
+
import { computeEffectiveThreshold } from "./hooks/adaptive-threshold.js";
|
|
19
|
+
import { loadSessionSavedTokens } from "./core/session-savings.js";
|
|
20
|
+
import { handleSaveDocCli, handleListDocsCli } from "./cli/save-doc.js";
|
|
21
|
+
import { checkForTypo } from "./cli/typo-guard.js";
|
|
22
|
+
import { processPostTask } from "./hooks/post-task.js";
|
|
23
|
+
import { isContextModeInstalledSync } from "./integration/context-mode-detector.js";
|
|
24
|
+
import { handleBlessAgents } from "./cli/bless-agents.js";
|
|
25
|
+
import { unblessAgents } from "./cli/unbless-agents.js";
|
|
26
|
+
import { detectDrift, formatDriftFinding } from "./cli/doctor-drift.js";
|
|
27
|
+
import { handleInstallAgents, maybeEmitStartupReminder, } from "./cli/install-agents.js";
|
|
28
|
+
import { handleUninstallAgents } from "./cli/uninstall-agents.js";
|
|
29
|
+
import { appendEvent, applyRetention, } from "./core/event-log.js";
|
|
30
|
+
import { handleStats } from "./cli/stats.js";
|
|
31
|
+
import { promptYesNo } from "./cli/install-agents.js";
|
|
32
|
+
import { runClaudeCodeEnvCheck } from "./cli/doctor-env-check.js";
|
|
33
|
+
import { claudeIgnoreStatus, writeDefaultClaudeIgnore, } from "./cli/claudeignore.js";
|
|
34
|
+
import { assessClaudeMd } from "./cli/claudemd-hygiene.js";
|
|
35
|
+
import { decidePostBashAdvice, renderPostBashHookOutput, } from "./hooks/post-bash.js";
|
|
13
36
|
const execFileAsync = promisify(execFile);
|
|
14
37
|
export const CODE_EXTENSIONS = new Set([
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
"ts",
|
|
39
|
+
"tsx",
|
|
40
|
+
"js",
|
|
41
|
+
"jsx",
|
|
42
|
+
"mjs",
|
|
43
|
+
"py",
|
|
44
|
+
"go",
|
|
45
|
+
"rs",
|
|
46
|
+
"java",
|
|
47
|
+
"kt",
|
|
48
|
+
"kts",
|
|
49
|
+
"swift",
|
|
50
|
+
"cs",
|
|
51
|
+
"cpp",
|
|
52
|
+
"cc",
|
|
53
|
+
"cxx",
|
|
54
|
+
"hpp",
|
|
55
|
+
"c",
|
|
56
|
+
"h",
|
|
57
|
+
"php",
|
|
58
|
+
"rb",
|
|
59
|
+
"scala",
|
|
60
|
+
"dart",
|
|
61
|
+
"lua",
|
|
62
|
+
"sh",
|
|
63
|
+
"bash",
|
|
64
|
+
"sql",
|
|
65
|
+
"r",
|
|
66
|
+
"vue",
|
|
67
|
+
"svelte",
|
|
68
|
+
"pl",
|
|
69
|
+
"pm",
|
|
70
|
+
"ex",
|
|
71
|
+
"exs",
|
|
72
|
+
"groovy",
|
|
73
|
+
"m",
|
|
74
|
+
"proto",
|
|
75
|
+
"bsl",
|
|
76
|
+
"lisp",
|
|
77
|
+
"lsp",
|
|
78
|
+
"cl",
|
|
79
|
+
"asd",
|
|
20
80
|
]);
|
|
21
81
|
export function getVersion() {
|
|
22
82
|
try {
|
|
23
|
-
const pkgPath = new URL(
|
|
24
|
-
const pkg = JSON.parse(readFileSync(pkgPath,
|
|
83
|
+
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
84
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
25
85
|
return pkg.version;
|
|
26
86
|
}
|
|
27
87
|
catch {
|
|
28
|
-
return
|
|
88
|
+
return "0.0.0";
|
|
29
89
|
}
|
|
30
90
|
}
|
|
31
91
|
export async function main(cliArgs = process.argv.slice(2)) {
|
|
92
|
+
// Guard against mis-typed commands like `install-aents` silently
|
|
93
|
+
// becoming a projectRoot=install-aents server launch. See TP-v0.22.3.
|
|
94
|
+
const typo = checkForTypo(cliArgs[0]);
|
|
95
|
+
if (typo.kind === "typo") {
|
|
96
|
+
process.stderr.write(`[token-pilot] ${typo.message}\n`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
32
99
|
switch (cliArgs[0]) {
|
|
33
|
-
case
|
|
100
|
+
case "hook-read": {
|
|
34
101
|
const cfg = await loadConfig(process.cwd());
|
|
35
|
-
handleHookRead(cliArgs[1], cfg.hooks.denyThreshold)
|
|
102
|
+
await handleHookRead(cliArgs[1], cfg.hooks.mode, cfg.hooks.denyThreshold, process.cwd(), {
|
|
103
|
+
adaptiveThreshold: cfg.hooks.adaptiveThreshold,
|
|
104
|
+
adaptiveBudgetTokens: cfg.hooks.adaptiveBudgetTokens,
|
|
105
|
+
});
|
|
36
106
|
return;
|
|
37
107
|
}
|
|
38
|
-
case
|
|
108
|
+
case "hook-edit":
|
|
39
109
|
handleHookEdit();
|
|
40
110
|
return;
|
|
41
|
-
case
|
|
111
|
+
case "hook-post-bash": {
|
|
112
|
+
try {
|
|
113
|
+
const stdin = readFileSync(0, "utf-8");
|
|
114
|
+
const input = JSON.parse(stdin);
|
|
115
|
+
const advice = decidePostBashAdvice(input, {
|
|
116
|
+
contextModeAvailable: isContextModeInstalledSync(process.cwd()),
|
|
117
|
+
});
|
|
118
|
+
const rendered = renderPostBashHookOutput(advice);
|
|
119
|
+
if (rendered)
|
|
120
|
+
process.stdout.write(rendered);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* silent — hook must not break */
|
|
124
|
+
}
|
|
125
|
+
process.exit(0);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
case "hook-post-task": {
|
|
129
|
+
try {
|
|
130
|
+
const stdin = readFileSync(0, "utf-8");
|
|
131
|
+
const input = JSON.parse(stdin);
|
|
132
|
+
const message = await processPostTask(process.cwd(), homedir(), input);
|
|
133
|
+
if (message) {
|
|
134
|
+
process.stdout.write(JSON.stringify({
|
|
135
|
+
hookSpecificOutput: {
|
|
136
|
+
hookEventName: "PostToolUse",
|
|
137
|
+
additionalContext: `[token-pilot] ${message}`,
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* silent — hook must not break */
|
|
144
|
+
}
|
|
145
|
+
process.exit(0);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "hook-session-start": {
|
|
149
|
+
const cfg = await loadConfig(process.cwd());
|
|
150
|
+
// `sessionStart.enabled` is independent of `hooks.mode` by design —
|
|
151
|
+
// a user may want the Read-blocking hook off (mode:"off") while still
|
|
152
|
+
// getting the tool-rules reminder at session start, or vice versa.
|
|
153
|
+
if (!cfg.sessionStart.enabled) {
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
const result = await handleSessionStart({
|
|
157
|
+
projectRoot: process.cwd(),
|
|
158
|
+
homeDir: homedir(),
|
|
159
|
+
sessionStartConfig: cfg.sessionStart,
|
|
160
|
+
});
|
|
161
|
+
if (result) {
|
|
162
|
+
process.stdout.write(result);
|
|
163
|
+
}
|
|
164
|
+
process.exit(0);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case "install-hook":
|
|
42
168
|
await handleInstallHook(cliArgs[1] || process.cwd());
|
|
43
169
|
return;
|
|
44
|
-
case
|
|
170
|
+
case "uninstall-hook":
|
|
45
171
|
await handleUninstallHook(cliArgs[1] || process.cwd());
|
|
46
172
|
return;
|
|
47
|
-
case
|
|
173
|
+
case "install-ast-index":
|
|
48
174
|
await handleInstallAstIndex();
|
|
49
175
|
return;
|
|
50
|
-
case
|
|
176
|
+
case "doctor":
|
|
51
177
|
await handleDoctor();
|
|
52
178
|
return;
|
|
53
|
-
case
|
|
179
|
+
case "bless-agents":
|
|
180
|
+
await handleBlessAgents(cliArgs.slice(1));
|
|
181
|
+
return;
|
|
182
|
+
case "unbless-agents": {
|
|
183
|
+
const args = cliArgs.slice(1);
|
|
184
|
+
const all = args.includes("--all");
|
|
185
|
+
const names = args.filter((a) => !a.startsWith("--"));
|
|
186
|
+
if (!all && names.length === 0) {
|
|
187
|
+
process.stderr.write("Usage: token-pilot unbless-agents <name>... | --all\n");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
await unblessAgents({ projectRoot: process.cwd(), names, all });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case "install-agents": {
|
|
194
|
+
const code = await handleInstallAgents(cliArgs.slice(1));
|
|
195
|
+
process.exit(code);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
case "uninstall-agents": {
|
|
199
|
+
const code = await handleUninstallAgents(cliArgs.slice(1));
|
|
200
|
+
process.exit(code);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
case "stats": {
|
|
204
|
+
const code = await handleStats(cliArgs.slice(1));
|
|
205
|
+
process.exit(code);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "save-doc": {
|
|
209
|
+
const code = await handleSaveDocCli(cliArgs.slice(1));
|
|
210
|
+
process.exit(code);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case "list-docs": {
|
|
214
|
+
const code = await handleListDocsCli();
|
|
215
|
+
process.exit(code);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
case "init":
|
|
54
219
|
await handleInit(cliArgs[1] || process.cwd());
|
|
55
220
|
return;
|
|
56
|
-
case
|
|
57
|
-
case
|
|
221
|
+
case "--version":
|
|
222
|
+
case "-v":
|
|
58
223
|
console.log(getVersion());
|
|
59
224
|
process.exit(0);
|
|
60
225
|
return;
|
|
61
|
-
case
|
|
62
|
-
case
|
|
226
|
+
case "--help":
|
|
227
|
+
case "-h":
|
|
63
228
|
printHelp();
|
|
64
229
|
return;
|
|
65
230
|
default:
|
|
@@ -76,20 +241,20 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
|
|
|
76
241
|
process.env.INIT_CWD, // npm/npx sets this to invoking directory
|
|
77
242
|
process.env.PWD, // shell working directory (may differ from cwd)
|
|
78
243
|
process.cwd(), // Node.js working directory
|
|
79
|
-
].filter((c) => !!c && c !==
|
|
244
|
+
].filter((c) => !!c && c !== "/");
|
|
80
245
|
let detected = false;
|
|
81
246
|
for (const candidate of candidates) {
|
|
82
247
|
if (isDangerousRoot(candidate))
|
|
83
248
|
continue;
|
|
84
249
|
try {
|
|
85
|
-
const { stdout } = await execFileAsync(
|
|
250
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
86
251
|
cwd: candidate,
|
|
87
252
|
timeout: 3000,
|
|
88
253
|
});
|
|
89
254
|
const gitRoot = stdout.trim();
|
|
90
255
|
if (gitRoot && !isDangerousRoot(gitRoot)) {
|
|
91
256
|
projectRoot = gitRoot;
|
|
92
|
-
console.error(`[token-pilot] project root: ${projectRoot} (git from ${candidate === process.env.INIT_CWD ?
|
|
257
|
+
console.error(`[token-pilot] project root: ${projectRoot} (git from ${candidate === process.env.INIT_CWD ? "INIT_CWD" : candidate === process.env.PWD ? "PWD" : "cwd"})`);
|
|
93
258
|
detected = true;
|
|
94
259
|
break;
|
|
95
260
|
}
|
|
@@ -100,10 +265,10 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
|
|
|
100
265
|
}
|
|
101
266
|
if (!detected) {
|
|
102
267
|
// Use best non-dangerous candidate as fallback even without git
|
|
103
|
-
const fallback = candidates.find(c => !isDangerousRoot(c));
|
|
268
|
+
const fallback = candidates.find((c) => !isDangerousRoot(c));
|
|
104
269
|
if (fallback) {
|
|
105
270
|
projectRoot = fallback;
|
|
106
|
-
console.error(`[token-pilot] project root: ${projectRoot} (${fallback === process.env.INIT_CWD ?
|
|
271
|
+
console.error(`[token-pilot] project root: ${projectRoot} (${fallback === process.env.INIT_CWD ? "INIT_CWD" : "PWD"}, not a git repo)`);
|
|
107
272
|
}
|
|
108
273
|
else {
|
|
109
274
|
console.error(`[token-pilot] project root: ${projectRoot} (cwd, not a git repo)`);
|
|
@@ -120,108 +285,222 @@ export async function startServer(cliArgs = process.argv.slice(2)) {
|
|
|
120
285
|
// Non-blocking update check for all components (logs to stderr, never blocks startup)
|
|
121
286
|
const config = await loadConfig(projectRoot);
|
|
122
287
|
const binaryStatus = await findBinary(config.astIndex.binaryPath);
|
|
123
|
-
checkAllUpdates(config, binaryStatus).catch(() => {
|
|
288
|
+
checkAllUpdates(config, binaryStatus).catch(() => {
|
|
289
|
+
/* ignore */
|
|
290
|
+
});
|
|
291
|
+
// Phase 5 subtask 5.6 — one-time reminder when no tp-* agents installed.
|
|
292
|
+
// Non-blocking, silent on error, single-fire per process.
|
|
293
|
+
maybeEmitStartupReminder({
|
|
294
|
+
projectRoot,
|
|
295
|
+
homeDir: homedir(),
|
|
296
|
+
configSuppressed: config.agents?.reminder === false,
|
|
297
|
+
}).catch(() => {
|
|
298
|
+
/* ignore */
|
|
299
|
+
});
|
|
300
|
+
// Phase 6 subtask 6.2 — age + size retention on hook-events archives.
|
|
301
|
+
// Fire-and-forget: retention failures must never block startup.
|
|
302
|
+
applyRetention(projectRoot).catch(() => {
|
|
303
|
+
/* ignore */
|
|
304
|
+
});
|
|
124
305
|
// Auto-install PreToolUse hook (non-blocking, Claude Code only)
|
|
125
306
|
// Uses absolute paths to node + script so hooks work in /bin/sh (nvm, npx, etc.)
|
|
126
307
|
let hookOptions;
|
|
127
308
|
try {
|
|
128
|
-
const rawPath = fileURLToPath(new URL(
|
|
129
|
-
hookOptions = {
|
|
309
|
+
const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
310
|
+
hookOptions = {
|
|
311
|
+
scriptPath: realpathSync(rawPath),
|
|
312
|
+
nodeExecPath: process.execPath,
|
|
313
|
+
};
|
|
130
314
|
}
|
|
131
315
|
catch {
|
|
132
316
|
// Can't resolve script path (e.g. running from src/ in tests) — fall back to bare command
|
|
133
317
|
}
|
|
134
|
-
installHook(projectRoot, hookOptions)
|
|
318
|
+
installHook(projectRoot, hookOptions)
|
|
319
|
+
.then((result) => {
|
|
135
320
|
if (result.installed) {
|
|
136
321
|
console.error(`[token-pilot] hook auto-installed: ${result.message}`);
|
|
137
322
|
}
|
|
138
|
-
})
|
|
323
|
+
})
|
|
324
|
+
.catch(() => {
|
|
325
|
+
/* ignore — not Claude Code or no .claude dir */
|
|
326
|
+
});
|
|
139
327
|
const server = await createServer(projectRoot, {
|
|
140
328
|
skipAstIndex: isDangerousRoot(projectRoot),
|
|
141
329
|
});
|
|
142
330
|
const transport = new StdioServerTransport();
|
|
143
331
|
await server.connect(transport);
|
|
144
|
-
process.on(
|
|
332
|
+
process.on("SIGINT", async () => {
|
|
145
333
|
await server.close();
|
|
146
334
|
process.exit(0);
|
|
147
335
|
});
|
|
148
|
-
process.on(
|
|
336
|
+
process.on("SIGTERM", async () => {
|
|
149
337
|
await server.close();
|
|
150
338
|
process.exit(0);
|
|
151
339
|
});
|
|
152
340
|
}
|
|
153
|
-
export function handleHookRead(filePathArg, denyThreshold = 300) {
|
|
154
|
-
//
|
|
341
|
+
export async function handleHookRead(filePathArg, mode = "deny-enhanced", denyThreshold = 300, projectRoot = process.cwd(), adaptive = {}) {
|
|
342
|
+
// Mode 'off' — hook is inert regardless of input.
|
|
343
|
+
if (mode === "off") {
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
346
|
+
const dispatchResult = await runHookReadDispatch(filePathArg, mode, denyThreshold, projectRoot, adaptive);
|
|
347
|
+
if (dispatchResult) {
|
|
348
|
+
process.stdout.write(dispatchResult);
|
|
349
|
+
}
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Pure implementation of the hook-read dispatch — returns the JSON payload
|
|
354
|
+
* to write to stdout, or null when we should pass-through (no output).
|
|
355
|
+
* Extracted for testability; the outer handleHookRead adds the process.exit
|
|
356
|
+
* wrapping.
|
|
357
|
+
*/
|
|
358
|
+
export async function runHookReadDispatch(filePathArg, mode, denyThresholdArg, projectRootArg, adaptive = {}) {
|
|
359
|
+
const denyThreshold = denyThresholdArg ?? 300;
|
|
360
|
+
const projectRoot = projectRootArg ?? process.cwd();
|
|
361
|
+
return runHookReadDispatchImpl(filePathArg, mode, denyThreshold, projectRoot, adaptive);
|
|
362
|
+
}
|
|
363
|
+
async function runHookReadDispatchImpl(filePathArg, mode, denyThreshold, projectRoot, adaptive = {}) {
|
|
364
|
+
if (mode === "off")
|
|
365
|
+
return null;
|
|
366
|
+
// Parse stdin to get tool_input + session/agent metadata, unless a
|
|
367
|
+
// filePath was supplied directly (tests, --filePath invocation).
|
|
155
368
|
let filePath = filePathArg;
|
|
156
369
|
let hasOffset = false;
|
|
157
370
|
let hasLimit = false;
|
|
371
|
+
let sessionId = "";
|
|
372
|
+
let agentType = null;
|
|
373
|
+
let agentId = null;
|
|
158
374
|
if (!filePath) {
|
|
159
375
|
try {
|
|
160
|
-
const stdin = readFileSync(0,
|
|
376
|
+
const stdin = readFileSync(0, "utf-8");
|
|
161
377
|
const input = JSON.parse(stdin);
|
|
162
378
|
filePath = input?.tool_input?.file_path;
|
|
163
379
|
hasOffset = input?.tool_input?.offset != null;
|
|
164
380
|
hasLimit = input?.tool_input?.limit != null;
|
|
381
|
+
sessionId = typeof input?.session_id === "string" ? input.session_id : "";
|
|
382
|
+
agentType =
|
|
383
|
+
typeof input?.agent_type === "string" ? input.agent_type : null;
|
|
384
|
+
agentId = typeof input?.agent_id === "string" ? input.agent_id : null;
|
|
165
385
|
}
|
|
166
386
|
catch {
|
|
167
|
-
|
|
387
|
+
return null;
|
|
168
388
|
}
|
|
169
389
|
}
|
|
170
|
-
if (!filePath)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
390
|
+
if (!filePath)
|
|
391
|
+
return null;
|
|
392
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
393
|
+
if (!CODE_EXTENSIONS.has(ext))
|
|
394
|
+
return null;
|
|
395
|
+
// Bounded Reads are always passed through — the agent already narrowed scope.
|
|
396
|
+
if (hasOffset || hasLimit)
|
|
397
|
+
return null;
|
|
398
|
+
// Path safety: refuse to summarise any file outside the project root
|
|
399
|
+
// (traversal, symlinks pointing outside). Pass-through on failure so the
|
|
400
|
+
// agent is never blocked by a safety reject.
|
|
401
|
+
if (!isPathWithinProject(filePath, projectRoot)) {
|
|
402
|
+
try {
|
|
403
|
+
process.stderr.write(`[token-pilot] refusing to summarise "${filePath}" — outside project root. Hook passing through.\n`);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
/* silent — hook must not break */
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
181
409
|
}
|
|
182
|
-
//
|
|
410
|
+
// Resolve effective threshold once (cheap if adaptive is off).
|
|
411
|
+
const effectiveThreshold = adaptive.adaptiveThreshold
|
|
412
|
+
? computeEffectiveThreshold({
|
|
413
|
+
baseThreshold: denyThreshold,
|
|
414
|
+
sessionSavedTokens: loadSessionSavedTokens(projectRoot, sessionId),
|
|
415
|
+
sessionBudgetTokens: adaptive.adaptiveBudgetTokens ?? 100_000,
|
|
416
|
+
enabled: true,
|
|
417
|
+
})
|
|
418
|
+
: denyThreshold;
|
|
419
|
+
// Read file content + line count.
|
|
420
|
+
let fileContent = "";
|
|
183
421
|
let lineCount = 0;
|
|
184
|
-
let fileContent = '';
|
|
185
422
|
try {
|
|
186
|
-
fileContent = readFileSync(filePath,
|
|
187
|
-
lineCount = fileContent.split(
|
|
188
|
-
if (lineCount <=
|
|
189
|
-
|
|
190
|
-
}
|
|
423
|
+
fileContent = readFileSync(filePath, "utf-8");
|
|
424
|
+
lineCount = fileContent.split("\n").length;
|
|
425
|
+
if (lineCount <= effectiveThreshold)
|
|
426
|
+
return null;
|
|
191
427
|
}
|
|
192
428
|
catch {
|
|
193
|
-
|
|
429
|
+
return null;
|
|
194
430
|
}
|
|
195
|
-
|
|
431
|
+
const charEst = Math.ceil(fileContent.length / 4);
|
|
432
|
+
const wsRatio = (fileContent.match(/\s/g)?.length ?? 0) / fileContent.length;
|
|
433
|
+
const estTokens = Math.ceil(charEst * (1 - wsRatio * 0.3));
|
|
434
|
+
// Legacy telemetry (hook-denied.jsonl) — retained for backward compatibility
|
|
435
|
+
// with existing loadDeniedReads() readers in session-analytics. Never block
|
|
436
|
+
// hook dispatch on failure.
|
|
196
437
|
try {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
438
|
+
const entry = JSON.stringify({
|
|
439
|
+
filePath,
|
|
440
|
+
lineCount,
|
|
441
|
+
estimatedTokens: estTokens,
|
|
442
|
+
mode,
|
|
443
|
+
timestamp: Date.now(),
|
|
444
|
+
});
|
|
445
|
+
const dir = join(projectRoot, ".token-pilot");
|
|
202
446
|
mkdirSync(dir, { recursive: true });
|
|
203
|
-
appendFileSync(join(dir,
|
|
447
|
+
appendFileSync(join(dir, "hook-denied.jsonl"), entry + "\n");
|
|
204
448
|
}
|
|
205
449
|
catch {
|
|
206
|
-
|
|
450
|
+
/* silent — hook must not break */
|
|
451
|
+
}
|
|
452
|
+
const writeEvent = async (eventKind, summaryTokens) => {
|
|
453
|
+
await appendEvent(projectRoot, {
|
|
454
|
+
ts: Date.now(),
|
|
455
|
+
session_id: sessionId,
|
|
456
|
+
agent_type: agentType,
|
|
457
|
+
agent_id: agentId,
|
|
458
|
+
event: eventKind,
|
|
459
|
+
file: filePath,
|
|
460
|
+
lines: lineCount,
|
|
461
|
+
estTokens,
|
|
462
|
+
summaryTokens,
|
|
463
|
+
savedTokens: Math.max(0, estTokens - summaryTokens),
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
if (mode === "advisory") {
|
|
467
|
+
const reason = `File "${filePath}" has ${lineCount} lines. Use mcp__token-pilot__smart_read("${filePath}") ` +
|
|
468
|
+
`for a structural overview, or mcp__token-pilot__read_for_edit("${filePath}", symbol="<name>") ` +
|
|
469
|
+
`for edit context. Bounded Read with offset/limit is still allowed.`;
|
|
470
|
+
await writeEvent("denied", Math.ceil(reason.length / 4));
|
|
471
|
+
return JSON.stringify({
|
|
472
|
+
hookSpecificOutput: {
|
|
473
|
+
hookEventName: "PreToolUse",
|
|
474
|
+
permissionDecision: "deny",
|
|
475
|
+
permissionDecisionReason: reason,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
207
478
|
}
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
479
|
+
// mode === 'deny-enhanced'
|
|
480
|
+
const pipelineResult = await runSummaryPipeline(fileContent, filePath);
|
|
481
|
+
if (pipelineResult.kind === "pass-through") {
|
|
482
|
+
await writeEvent("pass-through", 0);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const message = formatDenyMessage({
|
|
486
|
+
filePath,
|
|
487
|
+
summary: pipelineResult.summary,
|
|
488
|
+
tier: pipelineResult.tier,
|
|
489
|
+
});
|
|
490
|
+
await writeEvent("denied", Math.ceil(message.length / 4));
|
|
491
|
+
return JSON.stringify({
|
|
211
492
|
hookSpecificOutput: {
|
|
212
493
|
hookEventName: "PreToolUse",
|
|
213
494
|
permissionDecision: "deny",
|
|
214
|
-
permissionDecisionReason:
|
|
495
|
+
permissionDecisionReason: message,
|
|
215
496
|
},
|
|
216
497
|
});
|
|
217
|
-
process.stdout.write(deny);
|
|
218
|
-
process.exit(0);
|
|
219
498
|
}
|
|
220
499
|
export function handleHookEdit() {
|
|
221
500
|
// Parse stdin for Edit tool_input
|
|
222
501
|
let filePath;
|
|
223
502
|
try {
|
|
224
|
-
const stdin = readFileSync(0,
|
|
503
|
+
const stdin = readFileSync(0, "utf-8");
|
|
225
504
|
const input = JSON.parse(stdin);
|
|
226
505
|
filePath = input?.tool_input?.file_path;
|
|
227
506
|
}
|
|
@@ -231,7 +510,7 @@ export function handleHookEdit() {
|
|
|
231
510
|
if (!filePath) {
|
|
232
511
|
process.exit(0);
|
|
233
512
|
}
|
|
234
|
-
const ext = filePath.split(
|
|
513
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
235
514
|
// Only add context for code files
|
|
236
515
|
if (!CODE_EXTENSIONS.has(ext)) {
|
|
237
516
|
process.exit(0);
|
|
@@ -250,8 +529,11 @@ export function handleHookEdit() {
|
|
|
250
529
|
export async function handleInstallHook(projectRoot) {
|
|
251
530
|
let hookOptions;
|
|
252
531
|
try {
|
|
253
|
-
const rawPath = fileURLToPath(new URL(
|
|
254
|
-
hookOptions = {
|
|
532
|
+
const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
533
|
+
hookOptions = {
|
|
534
|
+
scriptPath: realpathSync(rawPath),
|
|
535
|
+
nodeExecPath: process.execPath,
|
|
536
|
+
};
|
|
255
537
|
}
|
|
256
538
|
catch {
|
|
257
539
|
// Fall back to bare command
|
|
@@ -290,23 +572,23 @@ export async function handleInstallAstIndex() {
|
|
|
290
572
|
}
|
|
291
573
|
export async function handleDoctor() {
|
|
292
574
|
const version = getVersion();
|
|
293
|
-
const { existsSync } = await import(
|
|
294
|
-
const { join } = await import(
|
|
575
|
+
const { existsSync } = await import("node:fs");
|
|
576
|
+
const { join } = await import("node:path");
|
|
295
577
|
const cwd = process.cwd();
|
|
296
578
|
console.log(`token-pilot doctor v${version}\n`);
|
|
297
579
|
// ── Environment ──
|
|
298
580
|
const nodeVersion = process.version;
|
|
299
581
|
const nodeMajor = parseInt(nodeVersion.slice(1), 10);
|
|
300
|
-
console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ?
|
|
301
|
-
const configPath = join(cwd,
|
|
302
|
-
console.log(`config: ${existsSync(configPath) ? configPath +
|
|
303
|
-
const gitDir = join(cwd,
|
|
304
|
-
console.log(`git repo: ${existsSync(gitDir) ?
|
|
305
|
-
console.log(
|
|
582
|
+
console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ? "✓" : "✗ (requires >=18)"}`);
|
|
583
|
+
const configPath = join(cwd, ".token-pilot.json");
|
|
584
|
+
console.log(`config: ${existsSync(configPath) ? configPath + " ✓" : "default (no .token-pilot.json)"}`);
|
|
585
|
+
const gitDir = join(cwd, ".git");
|
|
586
|
+
console.log(`git repo: ${existsSync(gitDir) ? "yes ✓" : "no (read_diff/git features unavailable)"}`);
|
|
587
|
+
console.log("");
|
|
306
588
|
// ── token-pilot ──
|
|
307
|
-
console.log(
|
|
589
|
+
console.log("── token-pilot ──");
|
|
308
590
|
console.log(` installed: ${version}`);
|
|
309
|
-
const tpLatest = await checkNpmLatest(
|
|
591
|
+
const tpLatest = await checkNpmLatest("token-pilot");
|
|
310
592
|
if (tpLatest) {
|
|
311
593
|
if (isNewerVersion(version, tpLatest)) {
|
|
312
594
|
console.log(` latest: ${tpLatest} (update available!)`);
|
|
@@ -319,9 +601,9 @@ export async function handleDoctor() {
|
|
|
319
601
|
else {
|
|
320
602
|
console.log(` latest: could not check (network error)`);
|
|
321
603
|
}
|
|
322
|
-
console.log(
|
|
604
|
+
console.log("");
|
|
323
605
|
// ── ast-index ──
|
|
324
|
-
console.log(
|
|
606
|
+
console.log("── ast-index ──");
|
|
325
607
|
const astStatus = await findBinary();
|
|
326
608
|
if (astStatus.available) {
|
|
327
609
|
console.log(` installed: ${astStatus.version} (${astStatus.source}: ${astStatus.path})`);
|
|
@@ -334,45 +616,99 @@ export async function handleDoctor() {
|
|
|
334
616
|
console.log(` latest: ${astUpdate.latest} ✓ (up to date)`);
|
|
335
617
|
}
|
|
336
618
|
const config = await loadConfig(cwd);
|
|
337
|
-
console.log(` auto-update: ${config.updates.autoUpdate ?
|
|
619
|
+
console.log(` auto-update: ${config.updates.autoUpdate ? "enabled ✓" : "disabled (set updates.autoUpdate=true in .token-pilot.json)"}`);
|
|
338
620
|
}
|
|
339
621
|
else {
|
|
340
622
|
console.log(` installed: not found ✗`);
|
|
341
623
|
console.log(` run: npx token-pilot install-ast-index`);
|
|
342
624
|
}
|
|
343
|
-
console.log(
|
|
625
|
+
console.log("");
|
|
344
626
|
// ── context-mode ──
|
|
345
|
-
console.log(
|
|
346
|
-
const { detectContextMode } = await import(
|
|
627
|
+
console.log("── context-mode ──");
|
|
628
|
+
const { detectContextMode } = await import("./integration/context-mode-detector.js");
|
|
347
629
|
const cmStatus = await detectContextMode(cwd);
|
|
348
|
-
console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` :
|
|
349
|
-
const cmLatest = await checkNpmLatest(
|
|
630
|
+
console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` : "no"}`);
|
|
631
|
+
const cmLatest = await checkNpmLatest("claude-context-mode");
|
|
350
632
|
if (cmLatest) {
|
|
351
633
|
console.log(` latest npm: ${cmLatest}`);
|
|
352
634
|
}
|
|
353
635
|
if (!cmStatus.detected) {
|
|
354
636
|
console.log(` setup: npx token-pilot init`);
|
|
355
637
|
}
|
|
356
|
-
console.log(
|
|
638
|
+
console.log("");
|
|
639
|
+
// ── blessed agents drift check ──
|
|
640
|
+
const drift = await detectDrift({ projectRoot: cwd, homeDir: homedir() });
|
|
641
|
+
if (drift.length > 0) {
|
|
642
|
+
console.log("── blessed-agents drift ──");
|
|
643
|
+
for (const finding of drift) {
|
|
644
|
+
console.log(` ${formatDriftFinding(finding)}`);
|
|
645
|
+
}
|
|
646
|
+
console.log("");
|
|
647
|
+
}
|
|
648
|
+
// ── Claude Code env-var savings tips ──
|
|
649
|
+
try {
|
|
650
|
+
const tips = await runClaudeCodeEnvCheck();
|
|
651
|
+
if (tips.length > 0) {
|
|
652
|
+
console.log("── Claude Code env knobs (savings tips) ──");
|
|
653
|
+
for (const t of tips) {
|
|
654
|
+
console.log(` ⚠ ${t}`);
|
|
655
|
+
}
|
|
656
|
+
console.log("");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
/* doctor must never crash over an optional check */
|
|
661
|
+
}
|
|
662
|
+
// ── .claudeignore ──
|
|
663
|
+
try {
|
|
664
|
+
const status = await claudeIgnoreStatus(cwd);
|
|
665
|
+
console.log("── .claudeignore ──");
|
|
666
|
+
if (status.kind === "absent") {
|
|
667
|
+
console.log(` not present — run \`npx token-pilot init\` to add sensible defaults`);
|
|
668
|
+
}
|
|
669
|
+
else if (status.kind === "managed") {
|
|
670
|
+
console.log(` present ✓ (managed by token-pilot; safe to edit)`);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
console.log(` present ✓ (user-owned; token-pilot will not touch it)`);
|
|
674
|
+
}
|
|
675
|
+
console.log("");
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
/* ignore */
|
|
679
|
+
}
|
|
680
|
+
// ── CLAUDE.md hygiene ──
|
|
681
|
+
try {
|
|
682
|
+
const r = await assessClaudeMd(cwd);
|
|
683
|
+
if (r.kind === "bloated") {
|
|
684
|
+
console.log("── CLAUDE.md hygiene ──");
|
|
685
|
+
console.log(` ⚠ ${r.path} has ${r.nonEmptyLines} non-empty lines (threshold: ${r.threshold}).`);
|
|
686
|
+
console.log(` This file loads into every Claude Code message — splitting into docs/*.md and loading on-demand saves tokens per turn.`);
|
|
687
|
+
console.log("");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
/* ignore */
|
|
692
|
+
}
|
|
357
693
|
process.exit(0);
|
|
358
694
|
}
|
|
359
695
|
export async function handleInit(targetDir) {
|
|
360
|
-
const { existsSync, readFileSync: readFs, writeFileSync } = await import(
|
|
361
|
-
const { join } = await import(
|
|
362
|
-
const mcpPath = join(targetDir,
|
|
696
|
+
const { existsSync, readFileSync: readFs, writeFileSync, } = await import("node:fs");
|
|
697
|
+
const { join } = await import("node:path");
|
|
698
|
+
const mcpPath = join(targetDir, ".mcp.json");
|
|
363
699
|
const tokenPilotConfig = {
|
|
364
|
-
command:
|
|
365
|
-
args: [
|
|
700
|
+
command: "npx",
|
|
701
|
+
args: ["-y", "token-pilot"],
|
|
366
702
|
};
|
|
367
703
|
const contextModeConfig = {
|
|
368
|
-
command:
|
|
369
|
-
args: [
|
|
704
|
+
command: "npx",
|
|
705
|
+
args: ["-y", "claude-context-mode"],
|
|
370
706
|
};
|
|
371
707
|
let config = { mcpServers: {} };
|
|
372
708
|
let existed = false;
|
|
373
709
|
if (existsSync(mcpPath)) {
|
|
374
710
|
try {
|
|
375
|
-
config = JSON.parse(readFs(mcpPath,
|
|
711
|
+
config = JSON.parse(readFs(mcpPath, "utf-8"));
|
|
376
712
|
if (!config.mcpServers)
|
|
377
713
|
config.mcpServers = {};
|
|
378
714
|
existed = true;
|
|
@@ -383,28 +719,71 @@ export async function handleInit(targetDir) {
|
|
|
383
719
|
}
|
|
384
720
|
}
|
|
385
721
|
const added = [];
|
|
386
|
-
if (!config.mcpServers[
|
|
387
|
-
config.mcpServers[
|
|
388
|
-
added.push(
|
|
722
|
+
if (!config.mcpServers["token-pilot"]) {
|
|
723
|
+
config.mcpServers["token-pilot"] = tokenPilotConfig;
|
|
724
|
+
added.push("token-pilot");
|
|
389
725
|
}
|
|
390
|
-
if (!config.mcpServers[
|
|
391
|
-
config.mcpServers[
|
|
392
|
-
added.push(
|
|
726
|
+
if (!config.mcpServers["context-mode"]) {
|
|
727
|
+
config.mcpServers["context-mode"] = contextModeConfig;
|
|
728
|
+
added.push("context-mode");
|
|
393
729
|
}
|
|
394
730
|
if (added.length === 0) {
|
|
395
731
|
console.log(`✓ ${mcpPath} already has both token-pilot and context-mode configured`);
|
|
396
732
|
process.exit(0);
|
|
397
733
|
}
|
|
398
|
-
writeFileSync(mcpPath, JSON.stringify(config, null, 2) +
|
|
734
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
399
735
|
if (existed) {
|
|
400
|
-
console.log(`✓ Updated ${mcpPath} — added: ${added.join(
|
|
736
|
+
console.log(`✓ Updated ${mcpPath} — added: ${added.join(", ")}`);
|
|
401
737
|
}
|
|
402
738
|
else {
|
|
403
739
|
console.log(`✓ Created ${mcpPath} with token-pilot + context-mode`);
|
|
404
740
|
}
|
|
405
741
|
console.log(`\nConfigured MCP servers:`);
|
|
406
|
-
console.log(` • token-pilot —
|
|
742
|
+
console.log(` • token-pilot — enforcement layer for token-efficient reads (hook + MCP + tp-* subagents)`);
|
|
407
743
|
console.log(` • context-mode — shell output & large data processing (BM25 sandbox)`);
|
|
744
|
+
// Claude Code users benefit from six token-pilot-native subagents (tp-run,
|
|
745
|
+
// tp-onboard, …). Offer installation now so they don't have to discover
|
|
746
|
+
// `install-agents` from a stderr reminder later.
|
|
747
|
+
const offeredAgents = added.includes("token-pilot") && process.stdin.isTTY === true;
|
|
748
|
+
if (offeredAgents) {
|
|
749
|
+
console.log("");
|
|
750
|
+
const yes = await promptYesNo("Install 6 tp-* subagents now (recommended for Claude Code)?", true);
|
|
751
|
+
if (yes) {
|
|
752
|
+
// Delegate to the full install-agents flow: it will prompt scope,
|
|
753
|
+
// handle idempotence, and persist the choice to .token-pilot.json.
|
|
754
|
+
const code = await handleInstallAgents([], { projectRoot: targetDir });
|
|
755
|
+
if (code !== 0) {
|
|
756
|
+
console.log("\n(install-agents returned non-zero; you can retry with: npx token-pilot install-agents)");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
console.log("\nSkipping agent install. Run later: npx token-pilot install-agents");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else if (added.includes("token-pilot")) {
|
|
764
|
+
// Non-TTY path — at minimum surface the next step in the log.
|
|
765
|
+
console.log(`\nNext step (Claude Code): npx token-pilot install-agents --scope=user|project`);
|
|
766
|
+
}
|
|
767
|
+
// Offer `.claudeignore` — small one-time win that compounds per message.
|
|
768
|
+
// Separate TTY check so a script that declined agents can still get ignore,
|
|
769
|
+
// and vice versa.
|
|
770
|
+
const ignoreStatus = await claudeIgnoreStatus(targetDir);
|
|
771
|
+
if (offeredAgents && ignoreStatus.kind !== "user-owned") {
|
|
772
|
+
console.log("");
|
|
773
|
+
const yesIgnore = await promptYesNo(ignoreStatus.kind === "absent"
|
|
774
|
+
? "Create .claudeignore with sensible defaults (node_modules, dist, lockfiles, …)?"
|
|
775
|
+
: "Refresh .claudeignore defaults (managed by token-pilot)?", true);
|
|
776
|
+
if (yesIgnore) {
|
|
777
|
+
const wrote = await writeDefaultClaudeIgnore(targetDir);
|
|
778
|
+
if (wrote)
|
|
779
|
+
console.log("✓ .claudeignore written");
|
|
780
|
+
else
|
|
781
|
+
console.log("(Skipped — existing .claudeignore is user-owned; not touched.)");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
else if (ignoreStatus.kind === "absent") {
|
|
785
|
+
console.log(`\nTip: \`npx token-pilot init\` offers to create .claudeignore — improves context by skipping node_modules, build artefacts, lockfiles.`);
|
|
786
|
+
}
|
|
408
787
|
console.log(`\nRestart your AI assistant to activate.`);
|
|
409
788
|
process.exit(0);
|
|
410
789
|
}
|
|
@@ -421,7 +800,7 @@ export async function checkNpmLatest(packageName) {
|
|
|
421
800
|
clearTimeout(timeout);
|
|
422
801
|
if (!resp.ok)
|
|
423
802
|
return null;
|
|
424
|
-
const data = await resp.json();
|
|
803
|
+
const data = (await resp.json());
|
|
425
804
|
return data.version ?? null;
|
|
426
805
|
}
|
|
427
806
|
catch {
|
|
@@ -432,59 +811,69 @@ export async function checkAllUpdates(config, binaryStatus) {
|
|
|
432
811
|
if (!config.updates.checkOnStartup)
|
|
433
812
|
return;
|
|
434
813
|
const [tpLatest, astUpdate, cmLatest] = await Promise.allSettled([
|
|
435
|
-
checkNpmLatest(
|
|
436
|
-
binaryStatus.available
|
|
437
|
-
|
|
814
|
+
checkNpmLatest("token-pilot"),
|
|
815
|
+
binaryStatus.available
|
|
816
|
+
? checkBinaryUpdate(binaryStatus.path)
|
|
817
|
+
: Promise.resolve(null),
|
|
818
|
+
checkNpmLatest("claude-context-mode"),
|
|
438
819
|
]);
|
|
439
820
|
// token-pilot
|
|
440
821
|
const tpVersion = getVersion();
|
|
441
|
-
if (tpLatest.status ===
|
|
822
|
+
if (tpLatest.status === "fulfilled" &&
|
|
823
|
+
tpLatest.value &&
|
|
824
|
+
isNewerVersion(tpVersion, tpLatest.value)) {
|
|
442
825
|
console.error(`[token-pilot] Update available: ${tpVersion} → ${tpLatest.value}. Run: npx token-pilot@latest`);
|
|
443
826
|
}
|
|
444
827
|
// ast-index
|
|
445
|
-
if (astUpdate.status ===
|
|
828
|
+
if (astUpdate.status === "fulfilled" && astUpdate.value?.updateAvailable) {
|
|
446
829
|
const { current, latest } = astUpdate.value;
|
|
447
830
|
if (config.updates.autoUpdate) {
|
|
448
831
|
console.error(`[token-pilot] Auto-updating ast-index: ${current} → ${latest}...`);
|
|
449
|
-
installBinary(msg => console.error(`[token-pilot] ${msg}`)).catch(() => { });
|
|
832
|
+
installBinary((msg) => console.error(`[token-pilot] ${msg}`)).catch(() => { });
|
|
450
833
|
}
|
|
451
834
|
else {
|
|
452
835
|
console.error(`[token-pilot] ast-index update: ${current} → ${latest}. Run: token-pilot install-ast-index`);
|
|
453
836
|
}
|
|
454
837
|
}
|
|
455
838
|
// context-mode (notification only — runs as separate MCP server)
|
|
456
|
-
if (cmLatest.status ===
|
|
839
|
+
if (cmLatest.status === "fulfilled" && cmLatest.value) {
|
|
457
840
|
// We can't reliably detect the currently installed version of context-mode
|
|
458
841
|
// (it runs as separate process via npx). Just log latest available for doctor.
|
|
459
842
|
// On startup, we only notify if explicitly useful.
|
|
460
843
|
}
|
|
461
844
|
}
|
|
462
845
|
export function printHelp() {
|
|
463
|
-
console.log(`token-pilot v${getVersion()} — MCP server for token-efficient code reading
|
|
464
|
-
|
|
465
|
-
Usage:
|
|
466
|
-
token-pilot [project-root] Start MCP server (default: cwd)
|
|
467
|
-
token-pilot init [dir] Create .mcp.json with token-pilot + context-mode
|
|
468
|
-
token-pilot install-hook [root] Install PreToolUse hook (Claude Code only)
|
|
469
|
-
token-pilot uninstall-hook [root] Remove PreToolUse hook
|
|
470
|
-
token-pilot install-ast-index Download ast-index binary (auto on first run)
|
|
471
|
-
token-pilot doctor Run diagnostics (check ast-index, config, updates)
|
|
472
|
-
token-pilot
|
|
473
|
-
token-pilot
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
846
|
+
console.log(`token-pilot v${getVersion()} — MCP server for token-efficient code reading
|
|
847
|
+
|
|
848
|
+
Usage:
|
|
849
|
+
token-pilot [project-root] Start MCP server (default: cwd)
|
|
850
|
+
token-pilot init [dir] Create .mcp.json with token-pilot + context-mode
|
|
851
|
+
token-pilot install-hook [root] Install PreToolUse hook (Claude Code only)
|
|
852
|
+
token-pilot uninstall-hook [root] Remove PreToolUse hook
|
|
853
|
+
token-pilot install-ast-index Download ast-index binary (auto on first run)
|
|
854
|
+
token-pilot doctor Run diagnostics (check ast-index, config, updates)
|
|
855
|
+
token-pilot save-doc <name> Save stdin to .token-pilot/docs/<name>.md
|
|
856
|
+
token-pilot list-docs List saved docs
|
|
857
|
+
token-pilot --version Show version
|
|
858
|
+
token-pilot --help Show this help
|
|
859
|
+
|
|
860
|
+
Quick start:
|
|
861
|
+
npx token-pilot init Setup .mcp.json (token-pilot + context-mode)
|
|
862
|
+
|
|
863
|
+
MCP Tools (22):
|
|
864
|
+
smart_read, read_symbol, read_symbols, read_range, read_section, read_diff,
|
|
865
|
+
read_for_edit, smart_read_many, find_usages, find_unused, related_files,
|
|
866
|
+
outline, project_overview, session_analytics, code_audit, module_info,
|
|
867
|
+
smart_diff, explore_area, smart_log, test_summary, session_snapshot,
|
|
868
|
+
session_budget
|
|
482
869
|
`);
|
|
483
870
|
process.exit(0);
|
|
484
871
|
}
|
|
485
|
-
const isDirectRun = process.argv[1] !== undefined &&
|
|
872
|
+
const isDirectRun = process.argv[1] !== undefined &&
|
|
873
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
874
|
+
realpathSync(process.argv[1]);
|
|
486
875
|
if (isDirectRun) {
|
|
487
|
-
main().catch(err => {
|
|
876
|
+
main().catch((err) => {
|
|
488
877
|
console.error(`[token-pilot] Fatal: ${err instanceof Error ? err.message : err}`);
|
|
489
878
|
process.exit(1);
|
|
490
879
|
});
|