troxy-cli 1.27.0 → 1.28.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 +9 -2
- package/src/mcp-server.js +1 -3
- package/src/pretooluse-hook.js +8 -1
- package/src/tests/hook-report.test.js +34 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "troxy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.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
|
@@ -42,6 +42,11 @@ export function extractUsage(entry) {
|
|
|
42
42
|
// dollar math server-side from the model id; this file only sums what
|
|
43
43
|
// the transcript says was used.
|
|
44
44
|
tokens: inputTokens + outputTokens + cacheCreation + cacheRead,
|
|
45
|
+
// Lives on the transcript ENTRY itself, not inside message.usage -
|
|
46
|
+
// confirmed live 2026-09-03 against a real session file (values seen:
|
|
47
|
+
// 'high', 'max'). Whatever string Claude Code actually used, passed
|
|
48
|
+
// through as-is; nothing here assumes a fixed low/medium/high vocabulary.
|
|
49
|
+
effort: entry.effort || null,
|
|
45
50
|
};
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -94,14 +99,16 @@ export function usageForCurrentTurn(transcriptPath) {
|
|
|
94
99
|
turnLines.push(entry);
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
const seen = new Map(); // messageId -> {model, tokens}
|
|
102
|
+
const seen = new Map(); // messageId -> {model, tokens, effort}
|
|
98
103
|
let lastModel = null;
|
|
104
|
+
let lastEffort = null;
|
|
99
105
|
for (const entry of turnLines) {
|
|
100
106
|
const usage = extractUsage(entry);
|
|
101
107
|
if (!usage || !usage.messageId) continue;
|
|
102
108
|
if (!seen.has(usage.messageId)) {
|
|
103
109
|
seen.set(usage.messageId, usage);
|
|
104
110
|
lastModel = lastModel || usage.model;
|
|
111
|
+
lastEffort = lastEffort || usage.effort;
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
|
|
@@ -130,7 +137,7 @@ export function usageForCurrentTurn(transcriptPath) {
|
|
|
130
137
|
}
|
|
131
138
|
const contentExcerpt = replyText ? replyText.slice(0, MAX_EXCERPT_LEN) : null;
|
|
132
139
|
|
|
133
|
-
return { tokens: totalTokens, model: lastModel, turnKey, contentExcerpt };
|
|
140
|
+
return { tokens: totalTokens, model: lastModel, effort: lastEffort, turnKey, contentExcerpt };
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
function withTimeout(promise, ms) {
|
package/src/mcp-server.js
CHANGED
|
@@ -336,8 +336,7 @@ export async function runMcp() {
|
|
|
336
336
|
},
|
|
337
337
|
effort: {
|
|
338
338
|
type: 'string',
|
|
339
|
-
|
|
340
|
-
description: 'How much work this task warrants. Report it honestly: "high effort on a simple task" is one of the things Troxy flags as waste.',
|
|
339
|
+
description: 'How much work this task warrants - your own real effort/thinking level for this turn (e.g. low, medium, high, or whatever your model calls it). Report it honestly: "high effort on a simple task" is one of the things Troxy flags as waste.',
|
|
341
340
|
},
|
|
342
341
|
task: {
|
|
343
342
|
type: 'string',
|
|
@@ -379,7 +378,6 @@ export async function runMcp() {
|
|
|
379
378
|
},
|
|
380
379
|
effort: {
|
|
381
380
|
type: 'string',
|
|
382
|
-
enum: ['low', 'medium', 'high'],
|
|
383
381
|
description: 'The effort level the task actually took, if it differed from what you declared up front (optional).',
|
|
384
382
|
},
|
|
385
383
|
},
|
package/src/pretooluse-hook.js
CHANGED
|
@@ -91,8 +91,15 @@ export async function runPreToolUseHook() {
|
|
|
91
91
|
const apiKey = process.env.TROXY_API_KEY || config?.apiKey;
|
|
92
92
|
if (!apiKey) return allow();
|
|
93
93
|
|
|
94
|
+
const body = { model: usage.model, task: payload.tool_name || '' };
|
|
95
|
+
// Real effort straight off the transcript entry (see hook-report.js's
|
|
96
|
+
// extractUsage) - whatever Claude Code actually used, not coerced into a
|
|
97
|
+
// guessed low/medium/high. Omitted when the transcript doesn't carry one
|
|
98
|
+
// (an older Claude Code build, or a turn before any assistant reply yet).
|
|
99
|
+
if (usage.effort) body.effort = usage.effort;
|
|
100
|
+
|
|
94
101
|
const result = await withTimeout(
|
|
95
|
-
api.evaluateModel(
|
|
102
|
+
api.evaluateModel(body, apiKey),
|
|
96
103
|
GATE_TIMEOUT_MS,
|
|
97
104
|
);
|
|
98
105
|
|
|
@@ -30,8 +30,10 @@ function writeLines(lines) {
|
|
|
30
30
|
fs.writeFileSync(transcriptPath(), lines.map(l => JSON.stringify(l)).join('\n') + '\n');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function assistantLine(messageId, usage, model = 'claude-sonnet-5') {
|
|
34
|
-
|
|
33
|
+
function assistantLine(messageId, usage, model = 'claude-sonnet-5', effort = undefined) {
|
|
34
|
+
const line = { type: 'assistant', message: { id: messageId, model, usage } };
|
|
35
|
+
if (effort !== undefined) line.effort = effort;
|
|
36
|
+
return line;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function assistantLineWithText(messageId, usage, text, model = 'claude-sonnet-5') {
|
|
@@ -41,9 +43,10 @@ function assistantLineWithText(messageId, usage, text, model = 'claude-sonnet-5'
|
|
|
41
43
|
const USAGE_A = { input_tokens: 2, output_tokens: 1176, cache_creation_input_tokens: 4134, cache_read_input_tokens: 244103 };
|
|
42
44
|
|
|
43
45
|
describe('extractUsage', () => {
|
|
44
|
-
it('only reads usage/model/id fields, never message.content', () => {
|
|
46
|
+
it('only reads usage/model/id/effort fields, never message.content', () => {
|
|
45
47
|
const entry = {
|
|
46
48
|
type: 'assistant',
|
|
49
|
+
effort: 'high',
|
|
47
50
|
message: {
|
|
48
51
|
id: 'msg_1',
|
|
49
52
|
model: 'claude-sonnet-5',
|
|
@@ -52,10 +55,19 @@ describe('extractUsage', () => {
|
|
|
52
55
|
},
|
|
53
56
|
};
|
|
54
57
|
const result = extractUsage(entry);
|
|
55
|
-
assert.deepEqual(Object.keys(result).sort(), ['messageId', 'model', 'tokens']);
|
|
58
|
+
assert.deepEqual(Object.keys(result).sort(), ['effort', 'messageId', 'model', 'tokens']);
|
|
59
|
+
assert.equal(result.effort, 'high');
|
|
56
60
|
assert.ok(!JSON.stringify(result).includes('actual conversation'));
|
|
57
61
|
});
|
|
58
62
|
|
|
63
|
+
it('effort is null when the entry does not carry one (older Claude Code build)', () => {
|
|
64
|
+
const entry = {
|
|
65
|
+
type: 'assistant',
|
|
66
|
+
message: { id: 'msg_1', model: 'claude-sonnet-5', usage: { input_tokens: 10, output_tokens: 5 } },
|
|
67
|
+
};
|
|
68
|
+
assert.equal(extractUsage(entry).effort, null);
|
|
69
|
+
});
|
|
70
|
+
|
|
59
71
|
it('returns null for a non-assistant line', () => {
|
|
60
72
|
assert.equal(extractUsage({ type: 'user', message: { usage: { input_tokens: 1 } } }), null);
|
|
61
73
|
});
|
|
@@ -112,6 +124,24 @@ describe('usageForCurrentTurn', () => {
|
|
|
112
124
|
assert.equal(result.model, 'claude-opus-4.5');
|
|
113
125
|
});
|
|
114
126
|
|
|
127
|
+
it('returns the effort from the assistant lines in the current turn', () => {
|
|
128
|
+
writeLines([
|
|
129
|
+
{ type: 'user', message: { content: [{ type: 'text', text: 'hi' }] } },
|
|
130
|
+
assistantLine('msg_1', { input_tokens: 1, output_tokens: 1 }, 'claude-opus-5', 'max'),
|
|
131
|
+
]);
|
|
132
|
+
const result = usageForCurrentTurn(transcriptPath());
|
|
133
|
+
assert.equal(result.effort, 'max');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('effort is null, not an error, on a transcript with no effort field (older Claude Code build)', () => {
|
|
137
|
+
writeLines([
|
|
138
|
+
{ type: 'user', message: { content: [{ type: 'text', text: 'hi' }] } },
|
|
139
|
+
assistantLine('msg_1', { input_tokens: 1, output_tokens: 1 }),
|
|
140
|
+
]);
|
|
141
|
+
const result = usageForCurrentTurn(transcriptPath());
|
|
142
|
+
assert.equal(result.effort, null);
|
|
143
|
+
});
|
|
144
|
+
|
|
115
145
|
it('returns a stable turn_key for the same set of message ids, order-independent', () => {
|
|
116
146
|
writeLines([
|
|
117
147
|
{ type: 'user', message: { content: [] } },
|