token-pilot 0.19.1 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/hooks/hooks.json +21 -0
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +736 -580
- 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/ast-index/binary-manager.d.ts +3 -3
- package/dist/ast-index/binary-manager.js +74 -11
- package/dist/ast-index/client.d.ts +5 -1
- package/dist/ast-index/client.js +9 -2
- 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.d.ts +7 -1
- package/dist/hooks/installer.js +175 -55
- 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 +508 -131
- 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 +89 -87
- 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,99 +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.)
|
|
278
|
+
let hookOptions;
|
|
279
|
+
try {
|
|
280
|
+
const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
281
|
+
hookOptions = {
|
|
282
|
+
scriptPath: realpathSync(rawPath),
|
|
283
|
+
nodeExecPath: process.execPath,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Can't resolve script path (e.g. running from src/ in tests) — fall back to bare command
|
|
288
|
+
}
|
|
289
|
+
installHook(projectRoot, hookOptions)
|
|
290
|
+
.then((result) => {
|
|
126
291
|
if (result.installed) {
|
|
127
292
|
console.error(`[token-pilot] hook auto-installed: ${result.message}`);
|
|
128
293
|
}
|
|
129
|
-
})
|
|
294
|
+
})
|
|
295
|
+
.catch(() => {
|
|
296
|
+
/* ignore — not Claude Code or no .claude dir */
|
|
297
|
+
});
|
|
130
298
|
const server = await createServer(projectRoot, {
|
|
131
299
|
skipAstIndex: isDangerousRoot(projectRoot),
|
|
132
300
|
});
|
|
133
301
|
const transport = new StdioServerTransport();
|
|
134
302
|
await server.connect(transport);
|
|
135
|
-
process.on(
|
|
303
|
+
process.on("SIGINT", async () => {
|
|
136
304
|
await server.close();
|
|
137
305
|
process.exit(0);
|
|
138
306
|
});
|
|
139
|
-
process.on(
|
|
307
|
+
process.on("SIGTERM", async () => {
|
|
140
308
|
await server.close();
|
|
141
309
|
process.exit(0);
|
|
142
310
|
});
|
|
143
311
|
}
|
|
144
|
-
export function handleHookRead(filePathArg, denyThreshold = 300) {
|
|
145
|
-
//
|
|
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).
|
|
146
339
|
let filePath = filePathArg;
|
|
147
340
|
let hasOffset = false;
|
|
148
341
|
let hasLimit = false;
|
|
342
|
+
let sessionId = "";
|
|
343
|
+
let agentType = null;
|
|
344
|
+
let agentId = null;
|
|
149
345
|
if (!filePath) {
|
|
150
346
|
try {
|
|
151
|
-
const stdin = readFileSync(0,
|
|
347
|
+
const stdin = readFileSync(0, "utf-8");
|
|
152
348
|
const input = JSON.parse(stdin);
|
|
153
349
|
filePath = input?.tool_input?.file_path;
|
|
154
350
|
hasOffset = input?.tool_input?.offset != null;
|
|
155
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;
|
|
156
356
|
}
|
|
157
357
|
catch {
|
|
158
|
-
|
|
358
|
+
return null;
|
|
159
359
|
}
|
|
160
360
|
}
|
|
161
|
-
if (!filePath)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
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;
|
|
172
380
|
}
|
|
173
|
-
//
|
|
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 = "";
|
|
174
392
|
let lineCount = 0;
|
|
175
|
-
let fileContent = '';
|
|
176
393
|
try {
|
|
177
|
-
fileContent = readFileSync(filePath,
|
|
178
|
-
lineCount = fileContent.split(
|
|
179
|
-
if (lineCount <=
|
|
180
|
-
|
|
181
|
-
}
|
|
394
|
+
fileContent = readFileSync(filePath, "utf-8");
|
|
395
|
+
lineCount = fileContent.split("\n").length;
|
|
396
|
+
if (lineCount <= effectiveThreshold)
|
|
397
|
+
return null;
|
|
182
398
|
}
|
|
183
399
|
catch {
|
|
184
|
-
|
|
400
|
+
return null;
|
|
185
401
|
}
|
|
186
|
-
|
|
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.
|
|
187
408
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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");
|
|
193
417
|
mkdirSync(dir, { recursive: true });
|
|
194
|
-
appendFileSync(join(dir,
|
|
418
|
+
appendFileSync(join(dir, "hook-denied.jsonl"), entry + "\n");
|
|
195
419
|
}
|
|
196
420
|
catch {
|
|
197
|
-
|
|
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
|
+
});
|
|
449
|
+
}
|
|
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;
|
|
198
455
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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({
|
|
202
463
|
hookSpecificOutput: {
|
|
203
464
|
hookEventName: "PreToolUse",
|
|
204
465
|
permissionDecision: "deny",
|
|
205
|
-
permissionDecisionReason:
|
|
466
|
+
permissionDecisionReason: message,
|
|
206
467
|
},
|
|
207
468
|
});
|
|
208
|
-
process.stdout.write(deny);
|
|
209
|
-
process.exit(0);
|
|
210
469
|
}
|
|
211
470
|
export function handleHookEdit() {
|
|
212
471
|
// Parse stdin for Edit tool_input
|
|
213
472
|
let filePath;
|
|
214
473
|
try {
|
|
215
|
-
const stdin = readFileSync(0,
|
|
474
|
+
const stdin = readFileSync(0, "utf-8");
|
|
216
475
|
const input = JSON.parse(stdin);
|
|
217
476
|
filePath = input?.tool_input?.file_path;
|
|
218
477
|
}
|
|
@@ -222,7 +481,7 @@ export function handleHookEdit() {
|
|
|
222
481
|
if (!filePath) {
|
|
223
482
|
process.exit(0);
|
|
224
483
|
}
|
|
225
|
-
const ext = filePath.split(
|
|
484
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
226
485
|
// Only add context for code files
|
|
227
486
|
if (!CODE_EXTENSIONS.has(ext)) {
|
|
228
487
|
process.exit(0);
|
|
@@ -239,7 +498,18 @@ export function handleHookEdit() {
|
|
|
239
498
|
process.exit(0);
|
|
240
499
|
}
|
|
241
500
|
export async function handleInstallHook(projectRoot) {
|
|
242
|
-
|
|
501
|
+
let hookOptions;
|
|
502
|
+
try {
|
|
503
|
+
const rawPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
504
|
+
hookOptions = {
|
|
505
|
+
scriptPath: realpathSync(rawPath),
|
|
506
|
+
nodeExecPath: process.execPath,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// Fall back to bare command
|
|
511
|
+
}
|
|
512
|
+
const result = await installHook(projectRoot, hookOptions);
|
|
243
513
|
console.log(result.message);
|
|
244
514
|
process.exit(result.fatal ? 1 : 0);
|
|
245
515
|
}
|
|
@@ -273,23 +543,23 @@ export async function handleInstallAstIndex() {
|
|
|
273
543
|
}
|
|
274
544
|
export async function handleDoctor() {
|
|
275
545
|
const version = getVersion();
|
|
276
|
-
const { existsSync } = await import(
|
|
277
|
-
const { join } = await import(
|
|
546
|
+
const { existsSync } = await import("node:fs");
|
|
547
|
+
const { join } = await import("node:path");
|
|
278
548
|
const cwd = process.cwd();
|
|
279
549
|
console.log(`token-pilot doctor v${version}\n`);
|
|
280
550
|
// ── Environment ──
|
|
281
551
|
const nodeVersion = process.version;
|
|
282
552
|
const nodeMajor = parseInt(nodeVersion.slice(1), 10);
|
|
283
|
-
console.log(`Node.js: ${nodeVersion} ${nodeMajor >= 18 ?
|
|
284
|
-
const configPath = join(cwd,
|
|
285
|
-
console.log(`config: ${existsSync(configPath) ? configPath +
|
|
286
|
-
const gitDir = join(cwd,
|
|
287
|
-
console.log(`git repo: ${existsSync(gitDir) ?
|
|
288
|
-
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("");
|
|
289
559
|
// ── token-pilot ──
|
|
290
|
-
console.log(
|
|
560
|
+
console.log("── token-pilot ──");
|
|
291
561
|
console.log(` installed: ${version}`);
|
|
292
|
-
const tpLatest = await checkNpmLatest(
|
|
562
|
+
const tpLatest = await checkNpmLatest("token-pilot");
|
|
293
563
|
if (tpLatest) {
|
|
294
564
|
if (isNewerVersion(version, tpLatest)) {
|
|
295
565
|
console.log(` latest: ${tpLatest} (update available!)`);
|
|
@@ -302,9 +572,9 @@ export async function handleDoctor() {
|
|
|
302
572
|
else {
|
|
303
573
|
console.log(` latest: could not check (network error)`);
|
|
304
574
|
}
|
|
305
|
-
console.log(
|
|
575
|
+
console.log("");
|
|
306
576
|
// ── ast-index ──
|
|
307
|
-
console.log(
|
|
577
|
+
console.log("── ast-index ──");
|
|
308
578
|
const astStatus = await findBinary();
|
|
309
579
|
if (astStatus.available) {
|
|
310
580
|
console.log(` installed: ${astStatus.version} (${astStatus.source}: ${astStatus.path})`);
|
|
@@ -317,45 +587,99 @@ export async function handleDoctor() {
|
|
|
317
587
|
console.log(` latest: ${astUpdate.latest} ✓ (up to date)`);
|
|
318
588
|
}
|
|
319
589
|
const config = await loadConfig(cwd);
|
|
320
|
-
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)"}`);
|
|
321
591
|
}
|
|
322
592
|
else {
|
|
323
593
|
console.log(` installed: not found ✗`);
|
|
324
594
|
console.log(` run: npx token-pilot install-ast-index`);
|
|
325
595
|
}
|
|
326
|
-
console.log(
|
|
596
|
+
console.log("");
|
|
327
597
|
// ── context-mode ──
|
|
328
|
-
console.log(
|
|
329
|
-
const { detectContextMode } = await import(
|
|
598
|
+
console.log("── context-mode ──");
|
|
599
|
+
const { detectContextMode } = await import("./integration/context-mode-detector.js");
|
|
330
600
|
const cmStatus = await detectContextMode(cwd);
|
|
331
|
-
console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` :
|
|
332
|
-
const cmLatest = await checkNpmLatest(
|
|
601
|
+
console.log(` detected: ${cmStatus.detected ? `yes (${cmStatus.source})` : "no"}`);
|
|
602
|
+
const cmLatest = await checkNpmLatest("claude-context-mode");
|
|
333
603
|
if (cmLatest) {
|
|
334
604
|
console.log(` latest npm: ${cmLatest}`);
|
|
335
605
|
}
|
|
336
606
|
if (!cmStatus.detected) {
|
|
337
607
|
console.log(` setup: npx token-pilot init`);
|
|
338
608
|
}
|
|
339
|
-
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
|
+
}
|
|
340
664
|
process.exit(0);
|
|
341
665
|
}
|
|
342
666
|
export async function handleInit(targetDir) {
|
|
343
|
-
const { existsSync, readFileSync: readFs, writeFileSync } = await import(
|
|
344
|
-
const { join } = await import(
|
|
345
|
-
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");
|
|
346
670
|
const tokenPilotConfig = {
|
|
347
|
-
command:
|
|
348
|
-
args: [
|
|
671
|
+
command: "npx",
|
|
672
|
+
args: ["-y", "token-pilot"],
|
|
349
673
|
};
|
|
350
674
|
const contextModeConfig = {
|
|
351
|
-
command:
|
|
352
|
-
args: [
|
|
675
|
+
command: "npx",
|
|
676
|
+
args: ["-y", "claude-context-mode"],
|
|
353
677
|
};
|
|
354
678
|
let config = { mcpServers: {} };
|
|
355
679
|
let existed = false;
|
|
356
680
|
if (existsSync(mcpPath)) {
|
|
357
681
|
try {
|
|
358
|
-
config = JSON.parse(readFs(mcpPath,
|
|
682
|
+
config = JSON.parse(readFs(mcpPath, "utf-8"));
|
|
359
683
|
if (!config.mcpServers)
|
|
360
684
|
config.mcpServers = {};
|
|
361
685
|
existed = true;
|
|
@@ -366,28 +690,71 @@ export async function handleInit(targetDir) {
|
|
|
366
690
|
}
|
|
367
691
|
}
|
|
368
692
|
const added = [];
|
|
369
|
-
if (!config.mcpServers[
|
|
370
|
-
config.mcpServers[
|
|
371
|
-
added.push(
|
|
693
|
+
if (!config.mcpServers["token-pilot"]) {
|
|
694
|
+
config.mcpServers["token-pilot"] = tokenPilotConfig;
|
|
695
|
+
added.push("token-pilot");
|
|
372
696
|
}
|
|
373
|
-
if (!config.mcpServers[
|
|
374
|
-
config.mcpServers[
|
|
375
|
-
added.push(
|
|
697
|
+
if (!config.mcpServers["context-mode"]) {
|
|
698
|
+
config.mcpServers["context-mode"] = contextModeConfig;
|
|
699
|
+
added.push("context-mode");
|
|
376
700
|
}
|
|
377
701
|
if (added.length === 0) {
|
|
378
702
|
console.log(`✓ ${mcpPath} already has both token-pilot and context-mode configured`);
|
|
379
703
|
process.exit(0);
|
|
380
704
|
}
|
|
381
|
-
writeFileSync(mcpPath, JSON.stringify(config, null, 2) +
|
|
705
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
382
706
|
if (existed) {
|
|
383
|
-
console.log(`✓ Updated ${mcpPath} — added: ${added.join(
|
|
707
|
+
console.log(`✓ Updated ${mcpPath} — added: ${added.join(", ")}`);
|
|
384
708
|
}
|
|
385
709
|
else {
|
|
386
710
|
console.log(`✓ Created ${mcpPath} with token-pilot + context-mode`);
|
|
387
711
|
}
|
|
388
712
|
console.log(`\nConfigured MCP servers:`);
|
|
389
|
-
console.log(` • token-pilot —
|
|
713
|
+
console.log(` • token-pilot — enforcement layer for token-efficient reads (hook + MCP + tp-* subagents)`);
|
|
390
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
|
+
}
|
|
391
758
|
console.log(`\nRestart your AI assistant to activate.`);
|
|
392
759
|
process.exit(0);
|
|
393
760
|
}
|
|
@@ -404,7 +771,7 @@ export async function checkNpmLatest(packageName) {
|
|
|
404
771
|
clearTimeout(timeout);
|
|
405
772
|
if (!resp.ok)
|
|
406
773
|
return null;
|
|
407
|
-
const data = await resp.json();
|
|
774
|
+
const data = (await resp.json());
|
|
408
775
|
return data.version ?? null;
|
|
409
776
|
}
|
|
410
777
|
catch {
|
|
@@ -415,28 +782,32 @@ export async function checkAllUpdates(config, binaryStatus) {
|
|
|
415
782
|
if (!config.updates.checkOnStartup)
|
|
416
783
|
return;
|
|
417
784
|
const [tpLatest, astUpdate, cmLatest] = await Promise.allSettled([
|
|
418
|
-
checkNpmLatest(
|
|
419
|
-
binaryStatus.available
|
|
420
|
-
|
|
785
|
+
checkNpmLatest("token-pilot"),
|
|
786
|
+
binaryStatus.available
|
|
787
|
+
? checkBinaryUpdate(binaryStatus.path)
|
|
788
|
+
: Promise.resolve(null),
|
|
789
|
+
checkNpmLatest("claude-context-mode"),
|
|
421
790
|
]);
|
|
422
791
|
// token-pilot
|
|
423
792
|
const tpVersion = getVersion();
|
|
424
|
-
if (tpLatest.status ===
|
|
793
|
+
if (tpLatest.status === "fulfilled" &&
|
|
794
|
+
tpLatest.value &&
|
|
795
|
+
isNewerVersion(tpVersion, tpLatest.value)) {
|
|
425
796
|
console.error(`[token-pilot] Update available: ${tpVersion} → ${tpLatest.value}. Run: npx token-pilot@latest`);
|
|
426
797
|
}
|
|
427
798
|
// ast-index
|
|
428
|
-
if (astUpdate.status ===
|
|
799
|
+
if (astUpdate.status === "fulfilled" && astUpdate.value?.updateAvailable) {
|
|
429
800
|
const { current, latest } = astUpdate.value;
|
|
430
801
|
if (config.updates.autoUpdate) {
|
|
431
802
|
console.error(`[token-pilot] Auto-updating ast-index: ${current} → ${latest}...`);
|
|
432
|
-
installBinary(msg => console.error(`[token-pilot] ${msg}`)).catch(() => { });
|
|
803
|
+
installBinary((msg) => console.error(`[token-pilot] ${msg}`)).catch(() => { });
|
|
433
804
|
}
|
|
434
805
|
else {
|
|
435
806
|
console.error(`[token-pilot] ast-index update: ${current} → ${latest}. Run: token-pilot install-ast-index`);
|
|
436
807
|
}
|
|
437
808
|
}
|
|
438
809
|
// context-mode (notification only — runs as separate MCP server)
|
|
439
|
-
if (cmLatest.status ===
|
|
810
|
+
if (cmLatest.status === "fulfilled" && cmLatest.value) {
|
|
440
811
|
// We can't reliably detect the currently installed version of context-mode
|
|
441
812
|
// (it runs as separate process via npx). Just log latest available for doctor.
|
|
442
813
|
// On startup, we only notify if explicitly useful.
|
|
@@ -452,22 +823,28 @@ Usage:
|
|
|
452
823
|
token-pilot uninstall-hook [root] Remove PreToolUse hook
|
|
453
824
|
token-pilot install-ast-index Download ast-index binary (auto on first run)
|
|
454
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
|
|
455
828
|
token-pilot --version Show version
|
|
456
829
|
token-pilot --help Show this help
|
|
457
830
|
|
|
458
831
|
Quick start:
|
|
459
832
|
npx token-pilot init Setup .mcp.json (token-pilot + context-mode)
|
|
460
833
|
|
|
461
|
-
MCP Tools (
|
|
462
|
-
smart_read, read_symbol,
|
|
463
|
-
|
|
464
|
-
|
|
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
|
|
465
840
|
`);
|
|
466
841
|
process.exit(0);
|
|
467
842
|
}
|
|
468
|
-
const isDirectRun = process.argv[1] !== undefined &&
|
|
843
|
+
const isDirectRun = process.argv[1] !== undefined &&
|
|
844
|
+
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
845
|
+
realpathSync(process.argv[1]);
|
|
469
846
|
if (isDirectRun) {
|
|
470
|
-
main().catch(err => {
|
|
847
|
+
main().catch((err) => {
|
|
471
848
|
console.error(`[token-pilot] Fatal: ${err instanceof Error ? err.message : err}`);
|
|
472
849
|
process.exit(1);
|
|
473
850
|
});
|