turntax 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/bin.js +5 -0
- package/dist/cli.js +210 -0
- package/dist/core/advice.js +447 -0
- package/dist/core/audit.js +98 -0
- package/dist/core/classify.js +252 -0
- package/dist/core/files.js +95 -0
- package/dist/core/markdown.js +180 -0
- package/dist/core/prices.js +163 -0
- package/dist/core/render.js +179 -0
- package/dist/core/tokenize.js +21 -0
- package/dist/core/tools.js +551 -0
- package/dist/core/types.js +1 -0
- package/dist/node/discover.js +63 -0
- package/dist/node/remote.js +100 -0
- package/package.json +61 -4
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages/count_tokens';
|
|
2
|
+
const ANTHROPIC_MESSAGES_URL = 'https://api.anthropic.com/v1/messages';
|
|
3
|
+
const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
4
|
+
const PROBE = [{ role: 'user', content: '.' }];
|
|
5
|
+
async function postJson(fetchFn, url, headers, body) {
|
|
6
|
+
const res = await fetchFn(url, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
9
|
+
body: JSON.stringify(body),
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
// Only the status goes into the message: response bodies can echo request content.
|
|
13
|
+
throw new Error(`HTTP ${res.status}`);
|
|
14
|
+
}
|
|
15
|
+
return res.json();
|
|
16
|
+
}
|
|
17
|
+
async function mapLimited(items, limit, fn) {
|
|
18
|
+
const results = new Array(items.length);
|
|
19
|
+
let next = 0;
|
|
20
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
21
|
+
while (next < items.length) {
|
|
22
|
+
const i = next++;
|
|
23
|
+
results[i] = await fn(items[i]);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
await Promise.all(workers);
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Exact Claude counts from Anthropic's count_tokens endpoint.
|
|
31
|
+
* Each text is sent as a system prompt next to a one-character user message; the same request
|
|
32
|
+
* without the system prompt is subtracted, so the result is the cost the text adds to a turn.
|
|
33
|
+
*/
|
|
34
|
+
export function anthropicCounter(apiKey, fetchFn = fetch) {
|
|
35
|
+
const headers = { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
|
|
36
|
+
const baselines = new Map();
|
|
37
|
+
return {
|
|
38
|
+
async count(model, texts) {
|
|
39
|
+
if (!model.apiModel)
|
|
40
|
+
return texts.map(() => ({ tokens: null, method: 'unavailable', reason: 'no API model id' }));
|
|
41
|
+
const apiModel = model.apiModel;
|
|
42
|
+
const baseline = () => {
|
|
43
|
+
if (!baselines.has(apiModel))
|
|
44
|
+
baselines.set(apiModel, postJson(fetchFn, ANTHROPIC_URL, headers, { model: apiModel, messages: PROBE }).then((r) => r.input_tokens));
|
|
45
|
+
return baselines.get(apiModel);
|
|
46
|
+
};
|
|
47
|
+
return mapLimited(texts, 4, async (text) => {
|
|
48
|
+
if (!text.trim())
|
|
49
|
+
return { tokens: 0, method: 'api' };
|
|
50
|
+
try {
|
|
51
|
+
const [base, full] = await Promise.all([
|
|
52
|
+
baseline(),
|
|
53
|
+
postJson(fetchFn, ANTHROPIC_URL, headers, { model: apiModel, system: text, messages: PROBE }),
|
|
54
|
+
]);
|
|
55
|
+
return { tokens: Math.max(0, full.input_tokens - base), method: 'api' };
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return { tokens: null, method: 'unavailable', reason: `Anthropic count_tokens failed: ${e.message}` };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Gemini counts from Google's models.countTokens endpoint. */
|
|
65
|
+
export function geminiCounter(apiKey, fetchFn = fetch) {
|
|
66
|
+
const headers = { 'x-goog-api-key': apiKey };
|
|
67
|
+
return {
|
|
68
|
+
async count(model, texts) {
|
|
69
|
+
if (!model.apiModel)
|
|
70
|
+
return texts.map(() => ({ tokens: null, method: 'unavailable', reason: 'no API model id' }));
|
|
71
|
+
const url = `${GEMINI_BASE}/${encodeURIComponent(model.apiModel)}:countTokens`;
|
|
72
|
+
return mapLimited(texts, 4, async (text) => {
|
|
73
|
+
if (!text.trim())
|
|
74
|
+
return { tokens: 0, method: 'api' };
|
|
75
|
+
try {
|
|
76
|
+
const r = await postJson(fetchFn, url, headers, { contents: [{ role: 'user', parts: [{ text }] }] });
|
|
77
|
+
return { tokens: r.totalTokens, method: 'api' };
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return { tokens: null, method: 'unavailable', reason: `Gemini countTokens failed: ${e.message}` };
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Opt-in English translation through Claude, used only to measure a language token multiplier. */
|
|
87
|
+
export async function translateToEnglish(apiKey, text, fetchFn = fetch) {
|
|
88
|
+
const r = await postJson(fetchFn, ANTHROPIC_MESSAGES_URL, { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' }, {
|
|
89
|
+
model: 'claude-haiku-5-5',
|
|
90
|
+
max_tokens: 8000,
|
|
91
|
+
system: 'Translate the user text into natural English. Keep markdown structure, code, paths and commands unchanged. Output only the translation.',
|
|
92
|
+
messages: [{ role: 'user', content: text }],
|
|
93
|
+
});
|
|
94
|
+
if (r.stop_reason === 'max_tokens')
|
|
95
|
+
throw new Error('translation was cut off (file too long)');
|
|
96
|
+
return (r.content ?? [])
|
|
97
|
+
.filter((b) => b.type === 'text')
|
|
98
|
+
.map((b) => b.text)
|
|
99
|
+
.join('');
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turntax",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Measure the per-turn token cost of AI coding-agent instruction files (AGENTS.md, CLAUDE.md, Cursor, Copilot, Cline, Roo, Kiro, Windsurf and more) for every agent that reads them.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "TORUT LTD",
|
|
7
|
+
"homepage": "https://github.com/TorutDev/turntax#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/TorutDev/turntax.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/TorutDev/turntax/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"turntax": "dist/bin.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.build.json",
|
|
27
|
+
"build:web": "esbuild src/web/main.ts --bundle --minify --format=esm --target=es2022 --outfile=web/app.js",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"prepack": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"agents-md",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"codex",
|
|
36
|
+
"cursor",
|
|
37
|
+
"copilot",
|
|
38
|
+
"gemini-cli",
|
|
39
|
+
"windsurf",
|
|
40
|
+
"cline",
|
|
41
|
+
"roo-code",
|
|
42
|
+
"kiro",
|
|
43
|
+
"junie",
|
|
44
|
+
"zed",
|
|
45
|
+
"continue",
|
|
46
|
+
"aider",
|
|
47
|
+
"goose",
|
|
48
|
+
"amp",
|
|
49
|
+
"opencode",
|
|
50
|
+
"tokens",
|
|
51
|
+
"context",
|
|
52
|
+
"cli"
|
|
53
|
+
],
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"gpt-tokenizer": "4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "24.19.0",
|
|
59
|
+
"esbuild": "0.28.2",
|
|
60
|
+
"typescript": "6.0.3",
|
|
61
|
+
"vitest": "5.0.3"
|
|
62
|
+
}
|
|
63
|
+
}
|