troxy-cli 1.28.0 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/hook-report.js +17 -1
- package/src/pretooluse-hook.js +9 -0
- package/src/tests/hook-report.test.js +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "troxy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
|
|
5
5
|
"homepage": "https://troxy.io",
|
|
6
6
|
"bugs": {
|
package/src/hook-report.js
CHANGED
|
@@ -102,6 +102,15 @@ export function usageForCurrentTurn(transcriptPath) {
|
|
|
102
102
|
const seen = new Map(); // messageId -> {model, tokens, effort}
|
|
103
103
|
let lastModel = null;
|
|
104
104
|
let lastEffort = null;
|
|
105
|
+
// How many tool_use content blocks this turn has produced so far -
|
|
106
|
+
// Claude Code writes one JSONL line per content block (see the big
|
|
107
|
+
// comment above), so this is a real count, not a guess, of tool calls
|
|
108
|
+
// already made this turn, before whichever one just triggered this hook.
|
|
109
|
+
// Only the block's `type` tag is read here, a category label
|
|
110
|
+
// ("tool_use"/"thinking"/"text"), never its actual name or arguments -
|
|
111
|
+
// same narrow-purpose exception extractReplyText already sets for
|
|
112
|
+
// touching `content` at all, still never touching real conversation data.
|
|
113
|
+
let toolUseBlocksSoFar = 0;
|
|
105
114
|
for (const entry of turnLines) {
|
|
106
115
|
const usage = extractUsage(entry);
|
|
107
116
|
if (!usage || !usage.messageId) continue;
|
|
@@ -110,6 +119,10 @@ export function usageForCurrentTurn(transcriptPath) {
|
|
|
110
119
|
lastModel = lastModel || usage.model;
|
|
111
120
|
lastEffort = lastEffort || usage.effort;
|
|
112
121
|
}
|
|
122
|
+
const content = entry?.type === 'assistant' ? entry.message?.content : null;
|
|
123
|
+
if (Array.isArray(content) && content.some(b => b?.type === 'tool_use')) {
|
|
124
|
+
toolUseBlocksSoFar++;
|
|
125
|
+
}
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
if (seen.size === 0) return null;
|
|
@@ -137,7 +150,10 @@ export function usageForCurrentTurn(transcriptPath) {
|
|
|
137
150
|
}
|
|
138
151
|
const contentExcerpt = replyText ? replyText.slice(0, MAX_EXCERPT_LEN) : null;
|
|
139
152
|
|
|
140
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
tokens: totalTokens, model: lastModel, effort: lastEffort, toolUseBlocksSoFar,
|
|
155
|
+
turnKey, contentExcerpt,
|
|
156
|
+
};
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
function withTimeout(promise, ms) {
|
package/src/pretooluse-hook.js
CHANGED
|
@@ -97,6 +97,15 @@ export async function runPreToolUseHook() {
|
|
|
97
97
|
// guessed low/medium/high. Omitted when the transcript doesn't carry one
|
|
98
98
|
// (an older Claude Code build, or a turn before any assistant reply yet).
|
|
99
99
|
if (usage.effort) body.effort = usage.effort;
|
|
100
|
+
// How many tools this turn has used so far (from the transcript), plus
|
|
101
|
+
// the one about to run that triggered this hook. An undercount by
|
|
102
|
+
// nature - a PreToolUse hook cannot know how many MORE tool calls this
|
|
103
|
+
// turn will end up making - but a real, current-as-of-right-now number,
|
|
104
|
+
// not a guess, and the effort-suggestion feature only needs to tell a
|
|
105
|
+
// turn's first tool call apart from its fifth, not know the eventual
|
|
106
|
+
// total. Without this, effort was accepted but never actually
|
|
107
|
+
// classified as "simple", so no suggestion could ever fire from here.
|
|
108
|
+
body.estimated_tool_count = usage.toolUseBlocksSoFar + 1;
|
|
100
109
|
|
|
101
110
|
const result = await withTimeout(
|
|
102
111
|
api.evaluateModel(body, apiKey),
|
|
@@ -40,6 +40,10 @@ function assistantLineWithText(messageId, usage, text, model = 'claude-sonnet-5'
|
|
|
40
40
|
return { type: 'assistant', message: { id: messageId, model, usage, content: [{ type: 'text', text }] } };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function assistantToolUseLine(messageId, usage, toolName = 'Bash', model = 'claude-sonnet-5') {
|
|
44
|
+
return { type: 'assistant', message: { id: messageId, model, usage, content: [{ type: 'tool_use', name: toolName }] } };
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
const USAGE_A = { input_tokens: 2, output_tokens: 1176, cache_creation_input_tokens: 4134, cache_read_input_tokens: 244103 };
|
|
44
48
|
|
|
45
49
|
describe('extractUsage', () => {
|
|
@@ -142,6 +146,29 @@ describe('usageForCurrentTurn', () => {
|
|
|
142
146
|
assert.equal(result.effort, null);
|
|
143
147
|
});
|
|
144
148
|
|
|
149
|
+
it('counts tool_use content blocks in the current turn, not text/thinking ones', () => {
|
|
150
|
+
writeLines([
|
|
151
|
+
{ type: 'user', message: { content: [{ type: 'text', text: 'hi' }] } },
|
|
152
|
+
// Same real shape: one JSONL line per content block, all three lines
|
|
153
|
+
// share message.id 'msg_1' and repeat the same usage object.
|
|
154
|
+
{ type: 'assistant', message: { id: 'msg_1', model: 'claude-sonnet-5', usage: { input_tokens: 1, output_tokens: 1 }, content: [{ type: 'thinking', thinking: 'planning' }] } },
|
|
155
|
+
assistantToolUseLine('msg_1', { input_tokens: 1, output_tokens: 1 }, 'Read'),
|
|
156
|
+
assistantToolUseLine('msg_2', { input_tokens: 1, output_tokens: 1 }, 'Bash'),
|
|
157
|
+
assistantLineWithText('msg_3', { input_tokens: 1, output_tokens: 1 }, 'done'),
|
|
158
|
+
]);
|
|
159
|
+
const result = usageForCurrentTurn(transcriptPath());
|
|
160
|
+
assert.equal(result.toolUseBlocksSoFar, 2);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('toolUseBlocksSoFar is 0 for a turn with no tool use yet', () => {
|
|
164
|
+
writeLines([
|
|
165
|
+
{ type: 'user', message: { content: [{ type: 'text', text: 'hi' }] } },
|
|
166
|
+
assistantLineWithText('msg_1', { input_tokens: 1, output_tokens: 1 }, 'just chatting'),
|
|
167
|
+
]);
|
|
168
|
+
const result = usageForCurrentTurn(transcriptPath());
|
|
169
|
+
assert.equal(result.toolUseBlocksSoFar, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
145
172
|
it('returns a stable turn_key for the same set of message ids, order-independent', () => {
|
|
146
173
|
writeLines([
|
|
147
174
|
{ type: 'user', message: { content: [] } },
|