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