token-pilot 0.19.1 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.claude-plugin/hooks/hooks.json +21 -0
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +736 -580
  4. package/README.md +172 -315
  5. package/dist/agents/tp-commit-writer.md +41 -0
  6. package/dist/agents/tp-dead-code-finder.md +43 -0
  7. package/dist/agents/tp-debugger.md +45 -0
  8. package/dist/agents/tp-impact-analyzer.md +44 -0
  9. package/dist/agents/tp-migration-scout.md +43 -0
  10. package/dist/agents/tp-onboard.md +40 -0
  11. package/dist/agents/tp-pr-reviewer.md +41 -0
  12. package/dist/agents/tp-refactor-planner.md +42 -0
  13. package/dist/agents/tp-run.md +48 -0
  14. package/dist/agents/tp-test-triage.md +40 -0
  15. package/dist/agents/tp-test-writer.md +46 -0
  16. package/dist/ast-index/binary-manager.d.ts +3 -3
  17. package/dist/ast-index/binary-manager.js +74 -11
  18. package/dist/ast-index/client.d.ts +5 -1
  19. package/dist/ast-index/client.js +9 -2
  20. package/dist/cli/agent-frontmatter.d.ts +48 -0
  21. package/dist/cli/agent-frontmatter.js +189 -0
  22. package/dist/cli/bless-agents.d.ts +65 -0
  23. package/dist/cli/bless-agents.js +307 -0
  24. package/dist/cli/claudeignore.d.ts +33 -0
  25. package/dist/cli/claudeignore.js +88 -0
  26. package/dist/cli/claudemd-hygiene.d.ts +26 -0
  27. package/dist/cli/claudemd-hygiene.js +43 -0
  28. package/dist/cli/doctor-drift.d.ts +31 -0
  29. package/dist/cli/doctor-drift.js +130 -0
  30. package/dist/cli/doctor-env-check.d.ts +25 -0
  31. package/dist/cli/doctor-env-check.js +91 -0
  32. package/dist/cli/install-agents.d.ts +108 -0
  33. package/dist/cli/install-agents.js +402 -0
  34. package/dist/cli/save-doc.d.ts +42 -0
  35. package/dist/cli/save-doc.js +145 -0
  36. package/dist/cli/scan-agents.d.ts +46 -0
  37. package/dist/cli/scan-agents.js +227 -0
  38. package/dist/cli/stats.d.ts +36 -0
  39. package/dist/cli/stats.js +131 -0
  40. package/dist/cli/unbless-agents.d.ts +33 -0
  41. package/dist/cli/unbless-agents.js +85 -0
  42. package/dist/cli/uninstall-agents.d.ts +36 -0
  43. package/dist/cli/uninstall-agents.js +117 -0
  44. package/dist/config/defaults.d.ts +1 -1
  45. package/dist/config/defaults.js +14 -8
  46. package/dist/config/loader.d.ts +1 -1
  47. package/dist/config/loader.js +105 -11
  48. package/dist/core/context-registry.d.ts +16 -1
  49. package/dist/core/context-registry.js +60 -28
  50. package/dist/core/event-log.d.ts +79 -0
  51. package/dist/core/event-log.js +190 -0
  52. package/dist/core/session-registry.d.ts +43 -0
  53. package/dist/core/session-registry.js +113 -0
  54. package/dist/core/session-savings.d.ts +19 -0
  55. package/dist/core/session-savings.js +60 -0
  56. package/dist/handlers/session-budget.d.ts +32 -0
  57. package/dist/handlers/session-budget.js +61 -0
  58. package/dist/handlers/session-snapshot-persist.d.ts +22 -0
  59. package/dist/handlers/session-snapshot-persist.js +76 -0
  60. package/dist/hooks/adaptive-threshold.d.ts +27 -0
  61. package/dist/hooks/adaptive-threshold.js +46 -0
  62. package/dist/hooks/format-deny-message.d.ts +21 -0
  63. package/dist/hooks/format-deny-message.js +147 -0
  64. package/dist/hooks/installer.d.ts +7 -1
  65. package/dist/hooks/installer.js +175 -55
  66. package/dist/hooks/path-safety.d.ts +16 -0
  67. package/dist/hooks/path-safety.js +34 -0
  68. package/dist/hooks/post-bash.d.ts +46 -0
  69. package/dist/hooks/post-bash.js +77 -0
  70. package/dist/hooks/session-start.d.ts +45 -0
  71. package/dist/hooks/session-start.js +179 -0
  72. package/dist/hooks/summary-ast-index.d.ts +28 -0
  73. package/dist/hooks/summary-ast-index.js +122 -0
  74. package/dist/hooks/summary-head-tail.d.ts +15 -0
  75. package/dist/hooks/summary-head-tail.js +78 -0
  76. package/dist/hooks/summary-pipeline.d.ts +35 -0
  77. package/dist/hooks/summary-pipeline.js +63 -0
  78. package/dist/hooks/summary-regex.d.ts +14 -0
  79. package/dist/hooks/summary-regex.js +130 -0
  80. package/dist/hooks/summary-types.d.ts +29 -0
  81. package/dist/hooks/summary-types.js +9 -0
  82. package/dist/index.d.ts +15 -3
  83. package/dist/index.js +508 -131
  84. package/dist/integration/context-mode-detector.d.ts +7 -1
  85. package/dist/integration/context-mode-detector.js +51 -15
  86. package/dist/server/tool-definitions.d.ts +149 -0
  87. package/dist/server/tool-definitions.js +424 -202
  88. package/dist/server.d.ts +1 -1
  89. package/dist/server.js +456 -179
  90. package/dist/templates/agent-builder.d.ts +49 -0
  91. package/dist/templates/agent-builder.js +104 -0
  92. package/dist/types.d.ts +38 -4
  93. package/package.json +89 -87
  94. package/skills/stats/SKILL.md +13 -2
@@ -8,11 +8,17 @@ export interface HookUninstallResult {
8
8
  fatal: boolean;
9
9
  message: string;
10
10
  }
11
+ export interface HookInstallOptions {
12
+ /** Absolute path to the entry script (dist/index.js). When provided, hooks use absolute paths instead of bare "token-pilot". */
13
+ scriptPath?: string;
14
+ /** Absolute path to the node binary. Defaults to process.execPath. */
15
+ nodeExecPath?: string;
16
+ }
11
17
  /**
12
18
  * Install Token Pilot hook into Claude Code settings.
13
19
  * Creates or updates .claude/settings.json with PreToolUse hook.
14
20
  */
15
- export declare function installHook(projectRoot: string): Promise<HookInstallResult>;
21
+ export declare function installHook(projectRoot: string, options?: HookInstallOptions): Promise<HookInstallResult>;
16
22
  /**
17
23
  * Remove Token Pilot hook from Claude Code settings.
18
24
  */
@@ -1,42 +1,87 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
- import { resolve, dirname } from 'node:path';
3
- const HOOK_CONFIG = {
4
- hooks: {
5
- PreToolUse: [
6
- {
7
- matcher: "Read",
8
- hooks: [
9
- {
10
- type: "command",
11
- command: "token-pilot hook-read",
12
- },
13
- ],
14
- },
15
- {
16
- matcher: "Edit",
17
- hooks: [
18
- {
19
- type: "command",
20
- command: "token-pilot hook-edit",
21
- },
22
- ],
23
- },
24
- ],
25
- },
26
- };
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { resolve, dirname } from "node:path";
3
+ /**
4
+ * Build hook command that works in any shell (/bin/sh, bash, etc.)
5
+ * Uses absolute paths to node + script to avoid PATH/nvm issues.
6
+ * Falls back to bare "token-pilot" only for manual CLI installs.
7
+ */
8
+ function buildHookCommand(action, options) {
9
+ if (options?.scriptPath) {
10
+ const node = options.nodeExecPath || process.execPath;
11
+ return `${node} ${options.scriptPath} ${action}`;
12
+ }
13
+ return `token-pilot ${action}`;
14
+ }
15
+ function createHookConfig(options) {
16
+ return {
17
+ hooks: {
18
+ PreToolUse: [
19
+ {
20
+ matcher: "Read",
21
+ hooks: [
22
+ {
23
+ type: "command",
24
+ command: buildHookCommand("hook-read", options),
25
+ },
26
+ ],
27
+ },
28
+ {
29
+ matcher: "Edit",
30
+ hooks: [
31
+ {
32
+ type: "command",
33
+ command: buildHookCommand("hook-edit", options),
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ SessionStart: [
39
+ {
40
+ hooks: [
41
+ {
42
+ type: "command",
43
+ command: buildHookCommand("hook-session-start", options),
44
+ },
45
+ ],
46
+ },
47
+ ],
48
+ PostToolUse: [
49
+ {
50
+ matcher: "Bash",
51
+ hooks: [
52
+ {
53
+ type: "command",
54
+ command: buildHookCommand("hook-post-bash", options),
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ };
61
+ }
27
62
  /**
28
63
  * Install Token Pilot hook into Claude Code settings.
29
64
  * Creates or updates .claude/settings.json with PreToolUse hook.
30
65
  */
31
- export async function installHook(projectRoot) {
32
- const settingsPath = resolve(projectRoot, '.claude', 'settings.json');
66
+ export async function installHook(projectRoot, options) {
67
+ // Skip auto-install when running as a Claude Code plugin —
68
+ // the plugin system already registers hooks via .claude-plugin/hooks/hooks.json
69
+ if (process.env.CLAUDE_PLUGIN_ROOT) {
70
+ return {
71
+ installed: false,
72
+ fatal: false,
73
+ message: "Running as plugin — hooks registered via plugin system.",
74
+ };
75
+ }
76
+ const settingsPath = resolve(projectRoot, ".claude", "settings.json");
77
+ const hookConfig = createHookConfig(options);
33
78
  try {
34
79
  // Ensure .claude dir exists
35
80
  await mkdir(dirname(settingsPath), { recursive: true });
36
81
  let settings = {};
37
82
  // Try to read existing settings
38
83
  try {
39
- const raw = await readFile(settingsPath, 'utf-8');
84
+ const raw = await readFile(settingsPath, "utf-8");
40
85
  try {
41
86
  settings = JSON.parse(raw);
42
87
  }
@@ -50,7 +95,7 @@ export async function installHook(projectRoot) {
50
95
  }
51
96
  }
52
97
  catch (err) {
53
- if (err?.code !== 'ENOENT') {
98
+ if (err?.code !== "ENOENT") {
54
99
  return {
55
100
  installed: false,
56
101
  fatal: true,
@@ -61,18 +106,36 @@ export async function installHook(projectRoot) {
61
106
  }
62
107
  // Check which Token Pilot hooks already exist
63
108
  const existingHooks = settings.hooks?.PreToolUse;
64
- const isTokenPilotHook = (h) => h.hooks?.some((hook) => hook.command?.includes('token-pilot'));
109
+ const isTokenPilotHook = (h) => h.hooks?.some((hook) => hook.command?.includes("token-pilot"));
65
110
  if (Array.isArray(existingHooks)) {
66
- const hasRead = existingHooks.some((h) => h.matcher === 'Read' && isTokenPilotHook(h));
67
- const hasEdit = existingHooks.some((h) => h.matcher === 'Edit' && isTokenPilotHook(h));
68
- if (hasRead && hasEdit) {
69
- return { installed: false, fatal: false, message: 'Token Pilot hooks already installed.' };
111
+ // Remove old broken hooks (bare "token-pilot" without absolute path)
112
+ // and replace with working ones using absolute paths
113
+ const oldBrokenHooks = existingHooks.filter((h) => isTokenPilotHook(h) &&
114
+ h.hooks?.some((hook) => hook.command?.match(/^token-pilot\s/)));
115
+ if (oldBrokenHooks.length > 0 && options?.scriptPath) {
116
+ // Remove old broken hooks, will re-add with absolute paths below
117
+ settings.hooks.PreToolUse = existingHooks.filter((h) => !isTokenPilotHook(h));
70
118
  }
71
- // Add missing hooks
72
- for (const hookDef of HOOK_CONFIG.hooks.PreToolUse) {
73
- const exists = existingHooks.some((h) => h.matcher === hookDef.matcher && isTokenPilotHook(h));
119
+ else {
120
+ const hasRead = existingHooks.some((h) => h.matcher === "Read" && isTokenPilotHook(h));
121
+ const hasEdit = existingHooks.some((h) => h.matcher === "Edit" && isTokenPilotHook(h));
122
+ const hasSessionStart = Array.isArray(settings.hooks?.SessionStart) &&
123
+ settings.hooks.SessionStart.some(isTokenPilotHook);
124
+ const hasPostBashHook = Array.isArray(settings.hooks?.PostToolUse) &&
125
+ settings.hooks.PostToolUse.some(isTokenPilotHook);
126
+ if (hasRead && hasEdit && hasSessionStart && hasPostBashHook) {
127
+ return {
128
+ installed: false,
129
+ fatal: false,
130
+ message: "Token Pilot hooks already installed.",
131
+ };
132
+ }
133
+ }
134
+ // Add missing PreToolUse hooks
135
+ for (const hookDef of hookConfig.hooks.PreToolUse) {
136
+ const exists = settings.hooks.PreToolUse.some((h) => h.matcher === hookDef.matcher && isTokenPilotHook(h));
74
137
  if (!exists) {
75
- existingHooks.push(hookDef);
138
+ settings.hooks.PreToolUse.push(hookDef);
76
139
  }
77
140
  }
78
141
  }
@@ -80,9 +143,32 @@ export async function installHook(projectRoot) {
80
143
  // Create hooks section
81
144
  if (!settings.hooks)
82
145
  settings.hooks = {};
83
- settings.hooks.PreToolUse = HOOK_CONFIG.hooks.PreToolUse;
146
+ settings.hooks.PreToolUse = hookConfig.hooks.PreToolUse;
147
+ }
148
+ // Install SessionStart hook idempotently
149
+ const existingSessionStart = settings.hooks?.SessionStart;
150
+ const hasSessionStart = Array.isArray(existingSessionStart) &&
151
+ existingSessionStart.some(isTokenPilotHook);
152
+ if (!hasSessionStart) {
153
+ if (!settings.hooks)
154
+ settings.hooks = {};
155
+ if (!Array.isArray(settings.hooks.SessionStart)) {
156
+ settings.hooks.SessionStart = [];
157
+ }
158
+ settings.hooks.SessionStart.push(...hookConfig.hooks.SessionStart);
159
+ }
160
+ // Install PostToolUse (Bash advisor) hook idempotently
161
+ const existingPost = settings.hooks?.PostToolUse;
162
+ const hasPostBash = Array.isArray(existingPost) && existingPost.some(isTokenPilotHook);
163
+ if (!hasPostBash) {
164
+ if (!settings.hooks)
165
+ settings.hooks = {};
166
+ if (!Array.isArray(settings.hooks.PostToolUse)) {
167
+ settings.hooks.PostToolUse = [];
168
+ }
169
+ settings.hooks.PostToolUse.push(...hookConfig.hooks.PostToolUse);
84
170
  }
85
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
171
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
86
172
  return {
87
173
  installed: true,
88
174
  fatal: false,
@@ -91,33 +177,63 @@ export async function installHook(projectRoot) {
91
177
  }
92
178
  catch (err) {
93
179
  const msg = err instanceof Error ? err.message : String(err);
94
- return { installed: false, fatal: true, message: `Failed to install hook: ${msg}` };
180
+ return {
181
+ installed: false,
182
+ fatal: true,
183
+ message: `Failed to install hook: ${msg}`,
184
+ };
95
185
  }
96
186
  }
97
187
  /**
98
188
  * Remove Token Pilot hook from Claude Code settings.
99
189
  */
100
190
  export async function uninstallHook(projectRoot) {
101
- const settingsPath = resolve(projectRoot, '.claude', 'settings.json');
191
+ const settingsPath = resolve(projectRoot, ".claude", "settings.json");
102
192
  try {
103
- const raw = await readFile(settingsPath, 'utf-8');
193
+ const raw = await readFile(settingsPath, "utf-8");
104
194
  const settings = JSON.parse(raw);
105
- if (!settings.hooks?.PreToolUse) {
106
- return { removed: false, fatal: false, message: 'No hooks to remove.' };
195
+ const hasPreToolUse = !!settings.hooks?.PreToolUse;
196
+ const hasSessionStart = !!settings.hooks?.SessionStart;
197
+ const hasPostToolUse = !!settings.hooks?.PostToolUse;
198
+ if (!hasPreToolUse && !hasSessionStart && !hasPostToolUse) {
199
+ return { removed: false, fatal: false, message: "No hooks to remove." };
107
200
  }
108
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !h.hooks?.some((hook) => hook.command?.includes('token-pilot')));
109
- if (settings.hooks.PreToolUse.length === 0) {
110
- delete settings.hooks.PreToolUse;
201
+ const isTokenPilotHook = (h) => h.hooks?.some((hook) => hook.command?.includes("token-pilot"));
202
+ if (Array.isArray(settings.hooks?.PreToolUse)) {
203
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !isTokenPilotHook(h));
204
+ if (settings.hooks.PreToolUse.length === 0) {
205
+ delete settings.hooks.PreToolUse;
206
+ }
207
+ }
208
+ if (Array.isArray(settings.hooks?.SessionStart)) {
209
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter((h) => !isTokenPilotHook(h));
210
+ if (settings.hooks.SessionStart.length === 0) {
211
+ delete settings.hooks.SessionStart;
212
+ }
213
+ }
214
+ if (Array.isArray(settings.hooks?.PostToolUse)) {
215
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((h) => !isTokenPilotHook(h));
216
+ if (settings.hooks.PostToolUse.length === 0) {
217
+ delete settings.hooks.PostToolUse;
218
+ }
111
219
  }
112
- if (Object.keys(settings.hooks).length === 0) {
220
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
113
221
  delete settings.hooks;
114
222
  }
115
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
116
- return { removed: true, fatal: false, message: 'Token Pilot hook removed.' };
223
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
224
+ return {
225
+ removed: true,
226
+ fatal: false,
227
+ message: "Token Pilot hook removed.",
228
+ };
117
229
  }
118
230
  catch (err) {
119
- if (err?.code === 'ENOENT') {
120
- return { removed: false, fatal: false, message: 'Settings file not found.' };
231
+ if (err?.code === "ENOENT") {
232
+ return {
233
+ removed: false,
234
+ fatal: false,
235
+ message: "Settings file not found.",
236
+ };
121
237
  }
122
238
  if (err instanceof SyntaxError) {
123
239
  return {
@@ -126,7 +242,11 @@ export async function uninstallHook(projectRoot) {
126
242
  message: `Settings file contains invalid JSON: ${settingsPath}. Fix it manually before uninstalling hooks.`,
127
243
  };
128
244
  }
129
- return { removed: false, fatal: true, message: `Failed to process settings: ${err?.message ?? err}` };
245
+ return {
246
+ removed: false,
247
+ fatal: true,
248
+ message: `Failed to process settings: ${err?.message ?? err}`,
249
+ };
130
250
  }
131
251
  }
132
252
  //# sourceMappingURL=installer.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path-safety check used by the PreToolUse hook before reading any file.
3
+ *
4
+ * Resolves both the target file and the project root through realpath
5
+ * (so symlinks cannot escape the sandbox), then requires the resolved
6
+ * file path to fall inside the resolved project directory. On any error
7
+ * (missing file, permission denied, realpath loop) we refuse — the hook
8
+ * will then pass-through rather than risk reading an attacker-crafted
9
+ * path.
10
+ *
11
+ * Sibling directories that share a common prefix (e.g. `/tmp/proj`
12
+ * vs `/tmp/proj-evil`) are rejected by forcing a path-separator on the
13
+ * normalised root.
14
+ */
15
+ export declare function isPathWithinProject(filePath: string, projectRoot: string): boolean;
16
+ //# sourceMappingURL=path-safety.d.ts.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Path-safety check used by the PreToolUse hook before reading any file.
3
+ *
4
+ * Resolves both the target file and the project root through realpath
5
+ * (so symlinks cannot escape the sandbox), then requires the resolved
6
+ * file path to fall inside the resolved project directory. On any error
7
+ * (missing file, permission denied, realpath loop) we refuse — the hook
8
+ * will then pass-through rather than risk reading an attacker-crafted
9
+ * path.
10
+ *
11
+ * Sibling directories that share a common prefix (e.g. `/tmp/proj`
12
+ * vs `/tmp/proj-evil`) are rejected by forcing a path-separator on the
13
+ * normalised root.
14
+ */
15
+ import { realpathSync } from "node:fs";
16
+ import { resolve, sep } from "node:path";
17
+ export function isPathWithinProject(filePath, projectRoot) {
18
+ if (!filePath || !projectRoot)
19
+ return false;
20
+ let resolvedFile;
21
+ let resolvedRoot;
22
+ try {
23
+ resolvedFile = realpathSync(resolve(filePath));
24
+ resolvedRoot = realpathSync(resolve(projectRoot));
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ if (resolvedFile === resolvedRoot)
30
+ return true;
31
+ const prefix = resolvedRoot.endsWith(sep) ? resolvedRoot : resolvedRoot + sep;
32
+ return resolvedFile.startsWith(prefix);
33
+ }
34
+ //# sourceMappingURL=path-safety.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * TP-jzh — Bash output advisor.
3
+ *
4
+ * Claude Code's PostToolUse hook cannot modify or truncate `tool_response`
5
+ * (verified via Claude Code docs 2026-04-18 — the `updatedMCPToolOutput`
6
+ * field is MCP-only). The agent has already seen the full stdout by the
7
+ * time our hook fires.
8
+ *
9
+ * So the feature becomes an *advisory*: when Bash stdout is large, we
10
+ * append one line via `additionalContext` pointing the agent at cheaper
11
+ * alternatives (`mcp__token-pilot__test_summary` for tests, bounded
12
+ * commands, head/tail piping). The agent notices before it repeats the
13
+ * mistake on the next turn.
14
+ */
15
+ export interface PostBashHookInput {
16
+ tool_name?: string;
17
+ tool_response?: unknown;
18
+ }
19
+ export interface PostBashAdvice {
20
+ /** Null when no advice is needed. */
21
+ additionalContext: string | null;
22
+ /** For telemetry: approximate output size the advisor saw. */
23
+ outputChars: number;
24
+ }
25
+ declare const LARGE_OUTPUT_THRESHOLD_CHARS = 8000;
26
+ /**
27
+ * Pure decision function. Given a PostToolUse hook input for the Bash
28
+ * tool, return advice text (or null to stay silent).
29
+ */
30
+ export interface PostBashAdviceOptions {
31
+ thresholdChars?: number;
32
+ /**
33
+ * When true, the advice also mentions context-mode — runs the command
34
+ * in a sandbox so only stdout enters context. Caller detects whether
35
+ * context-mode is installed and passes the flag.
36
+ */
37
+ contextModeAvailable?: boolean;
38
+ }
39
+ export declare function decidePostBashAdvice(input: PostBashHookInput, thresholdCharsOrOpts?: number | PostBashAdviceOptions): PostBashAdvice;
40
+ /**
41
+ * Render the JSON payload Claude Code expects. Returns null for silent
42
+ * pass-through so the caller can simply `exit(0)` with no stdout.
43
+ */
44
+ export declare function renderPostBashHookOutput(advice: PostBashAdvice): string | null;
45
+ export { LARGE_OUTPUT_THRESHOLD_CHARS };
46
+ //# sourceMappingURL=post-bash.d.ts.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * TP-jzh — Bash output advisor.
3
+ *
4
+ * Claude Code's PostToolUse hook cannot modify or truncate `tool_response`
5
+ * (verified via Claude Code docs 2026-04-18 — the `updatedMCPToolOutput`
6
+ * field is MCP-only). The agent has already seen the full stdout by the
7
+ * time our hook fires.
8
+ *
9
+ * So the feature becomes an *advisory*: when Bash stdout is large, we
10
+ * append one line via `additionalContext` pointing the agent at cheaper
11
+ * alternatives (`mcp__token-pilot__test_summary` for tests, bounded
12
+ * commands, head/tail piping). The agent notices before it repeats the
13
+ * mistake on the next turn.
14
+ */
15
+ const LARGE_OUTPUT_THRESHOLD_CHARS = 8000;
16
+ function extractStdout(tool_response) {
17
+ if (!tool_response)
18
+ return "";
19
+ if (typeof tool_response === "string")
20
+ return tool_response;
21
+ if (typeof tool_response === "object") {
22
+ const r = tool_response;
23
+ const parts = [];
24
+ for (const key of ["stdout", "output", "content"]) {
25
+ const v = r[key];
26
+ if (typeof v === "string")
27
+ parts.push(v);
28
+ }
29
+ return parts.join("\n");
30
+ }
31
+ return "";
32
+ }
33
+ function countLines(s) {
34
+ if (s === "")
35
+ return 0;
36
+ return s.split(/\r?\n/).length;
37
+ }
38
+ export function decidePostBashAdvice(input, thresholdCharsOrOpts = LARGE_OUTPUT_THRESHOLD_CHARS) {
39
+ const opts = typeof thresholdCharsOrOpts === "number"
40
+ ? { thresholdChars: thresholdCharsOrOpts }
41
+ : thresholdCharsOrOpts;
42
+ const threshold = opts.thresholdChars ?? LARGE_OUTPUT_THRESHOLD_CHARS;
43
+ if (input.tool_name !== "Bash") {
44
+ return { additionalContext: null, outputChars: 0 };
45
+ }
46
+ const stdout = extractStdout(input.tool_response);
47
+ const chars = stdout.length;
48
+ if (chars < threshold) {
49
+ return { additionalContext: null, outputChars: chars };
50
+ }
51
+ const lines = countLines(stdout);
52
+ const roughTokens = Math.ceil(chars / 4);
53
+ const contextModeLine = opts.contextModeAvailable
54
+ ? " Or run via mcp__context-mode__execute — sandbox keeps stdout out of your window."
55
+ : "";
56
+ const msg = `⚠ Bash output was large (~${lines} lines, ~${roughTokens} tokens). ` +
57
+ `Consider mcp__token-pilot__test_summary for test runs, or bounded commands ` +
58
+ `(head/tail, --oneline, git log -n <N>, grep -m <N>) to keep context lean.` +
59
+ contextModeLine;
60
+ return { additionalContext: msg, outputChars: chars };
61
+ }
62
+ /**
63
+ * Render the JSON payload Claude Code expects. Returns null for silent
64
+ * pass-through so the caller can simply `exit(0)` with no stdout.
65
+ */
66
+ export function renderPostBashHookOutput(advice) {
67
+ if (!advice.additionalContext)
68
+ return null;
69
+ return JSON.stringify({
70
+ hookSpecificOutput: {
71
+ hookEventName: "PostToolUse",
72
+ additionalContext: advice.additionalContext,
73
+ },
74
+ });
75
+ }
76
+ export { LARGE_OUTPUT_THRESHOLD_CHARS };
77
+ //# sourceMappingURL=post-bash.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SessionStart reminder hook — Component 2 of the enforcement layer.
3
+ *
4
+ * On every session start / /clear / /compact, emits a compact additionalContext
5
+ * block containing the mandatory-tool rules and a list of tp-* subagents found
6
+ * in the project and user agent directories.
7
+ *
8
+ * Output contract: one JSON line on stdout, or exit 0 silent.
9
+ */
10
+ export interface AgentEntry {
11
+ name: string;
12
+ description: string;
13
+ }
14
+ export interface SessionStartConfig {
15
+ enabled: boolean;
16
+ showStats: boolean;
17
+ maxReminderTokens: number;
18
+ }
19
+ export interface HandleSessionStartOptions {
20
+ projectRoot: string;
21
+ homeDir: string;
22
+ sessionStartConfig: SessionStartConfig;
23
+ }
24
+ /**
25
+ * Scan ~/.claude/agents/ and ./.claude/agents/ for tp-*.md agent definitions.
26
+ * Project directory takes precedence; duplicates (by name) are dropped.
27
+ *
28
+ * @param projectRoot - absolute path to the project root
29
+ * @param homeDir - home directory (injected for testability; defaults to os.homedir())
30
+ */
31
+ export declare function scanAgents(projectRoot: string, homeDir: string): Promise<AgentEntry[]>;
32
+ /**
33
+ * Build the reminder message combining the mandatory-tool rules and the
34
+ * tp-* agent list. Enforces the maxReminderTokens budget by trimming the
35
+ * delegating list with "… and N more" if needed.
36
+ */
37
+ export declare function buildReminderMessage(agents: AgentEntry[], maxReminderTokens: number): string;
38
+ /**
39
+ * Main handler for the hook-session-start CLI command.
40
+ *
41
+ * Returns the JSON string to write to stdout, or null for silent exit.
42
+ * Never throws — any error → null (fail-safe pass-through).
43
+ */
44
+ export declare function handleSessionStart(opts: HandleSessionStartOptions): Promise<string | null>;
45
+ //# sourceMappingURL=session-start.d.ts.map