turntax 0.1.0 → 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/README.md +138 -111
- package/dist/core/advice.js +1 -1
- package/dist/core/classify.js +48 -7
- package/dist/core/files.js +26 -5
- package/dist/core/markdown.js +10 -0
- package/dist/core/render.js +52 -16
- package/dist/core/tools.js +250 -0
- package/package.json +63 -53
package/README.md
CHANGED
|
@@ -1,111 +1,138 @@
|
|
|
1
|
-
# turntax
|
|
2
|
-
|
|
3
|
-
Coding agents re-send their instruction files with every turn. `turntax` measures that fixed per-turn cost for a repository,
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npx turntax
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1
|
+
# turntax
|
|
2
|
+
|
|
3
|
+
Coding agents re-send their instruction files with every turn. `turntax` measures that fixed per-turn cost for a repository, for every agent that reads it, and shows where to cut it.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx turntax
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Try it in the browser, with nothing uploaded: https://torutdev.github.io/turntax/
|
|
10
|
+
|
|
11
|
+
## Supported agents
|
|
12
|
+
|
|
13
|
+
Each agent's documented loading rules decide what is sent on every turn, what loads only when the agent works in a folder or touches a matching file, and what is ignored.
|
|
14
|
+
|
|
15
|
+
| Agent | Files it reads from the repository |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Aider | files listed under `read:` in `.aider.conf.yml` (`CONVENTIONS.md` only when listed or passed with `--read`) |
|
|
18
|
+
| Amp | `AGENTS.md` (or `AGENT.md` / `CLAUDE.md`), nested `AGENTS.md`, @-mentioned files |
|
|
19
|
+
| Claude Code | `CLAUDE.md`, `CLAUDE.local.md`, `.claude/rules`, skills, commands, subagents, `AGENTS.md` when there is no `CLAUDE.md` |
|
|
20
|
+
| Cline | `.clinerules/`, `.cline/rules/` |
|
|
21
|
+
| Continue | `.continue/rules/` |
|
|
22
|
+
| Cursor | `.cursor/rules/*.mdc`, `.cursorrules`, `AGENTS.md`, `CLAUDE.md` |
|
|
23
|
+
| Gemini CLI | `GEMINI.md` |
|
|
24
|
+
| GitHub Copilot | `.github/copilot-instructions.md`, `.github/instructions/`, `AGENTS.md` |
|
|
25
|
+
| Goose | `.goosehints`, `AGENTS.md` |
|
|
26
|
+
| JetBrains Junie | `.junie/AGENTS.md`, or `AGENTS.md` with `.junie/rules/`, or `.junie/guidelines` |
|
|
27
|
+
| Kiro | `.kiro/steering/`, `AGENTS.md` |
|
|
28
|
+
| OpenAI Codex | `AGENTS.md`, `AGENTS.override.md`, `.agents/skills` |
|
|
29
|
+
| OpenCode | `AGENTS.md`, or `CLAUDE.md` when there is none |
|
|
30
|
+
| Roo Code | `.roo/rules/`, `.roo/rules-{mode}/`, `.roorules`, `AGENTS.md` |
|
|
31
|
+
| Windsurf (Devin Desktop) | `.devin/rules/`, `.windsurf/rules/`, `.windsurfrules`, `AGENTS.md` |
|
|
32
|
+
| Zed | the first of `.rules`, `.cursorrules`, `.windsurfrules`, `.clinerules`, `.github/copilot-instructions.md`, `AGENT.md`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md` |
|
|
33
|
+
|
|
34
|
+
Agents that load exactly the same files share one row in the report:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
always o200k/turn on-demand model tokens/turn counted by 100 turns cached agents
|
|
38
|
+
4 272 53 (no selected model) Claude Code
|
|
39
|
+
1 63 0 GPT-6.1 Sol 63 estimate (o200k_base proxy) $0.01 $0.0006 OpenAI Codex, Roo Code, Kiro, JetBrains Junie, Goose, OpenCode
|
|
40
|
+
1 17 0 (no selected model) Gemini CLI
|
|
41
|
+
4 297 37 GPT-6.1 Sol 297 estimate (o200k_base proxy) $0.06 $0.0030 Cursor
|
|
42
|
+
2 74 8 GPT-6.1 Sol 74 estimate (o200k_base proxy) $0.01 $0.0007 GitHub Copilot
|
|
43
|
+
2 75 0 GPT-6.1 Sol 75 estimate (o200k_base proxy) $0.01 $0.0008 Windsurf (Devin Desktop)
|
|
44
|
+
1 11 0 GPT-6.1 Sol 11 estimate (o200k_base proxy) $0.0022 $0.0001 Zed
|
|
45
|
+
1 63 19 GPT-6.1 Sol 63 estimate (o200k_base proxy) $0.01 $0.0006 Amp
|
|
46
|
+
|
|
47
|
+
Advice (savings are estimates in o200k tokens per turn)
|
|
48
|
+
HIGH duplicate CLAUDE.md, AGENTS.md [saves ~48 tokens/turn]
|
|
49
|
+
3 identical lines are loaded twice every turn by cursor. Keep them in one file.
|
|
50
|
+
HIGH contradiction AGENTS.md, CLAUDE.md [saves ~11 tokens/turn]
|
|
51
|
+
Conflicting package manager: "Always use npm ..." (AGENTS.md:3) vs "Always use pnpm ..." (CLAUDE.md:5).
|
|
52
|
+
MEDIUM scopable-section CLAUDE.md:11 [saves ~128 tokens/turn]
|
|
53
|
+
Section "API package" is mostly about packages/api/. Move it to packages/api/AGENTS.md ...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
(Excerpt of the output for [`test/fixtures/mixed`](test/fixtures/mixed) with `--models gpt-6.1-sol`.)
|
|
57
|
+
|
|
58
|
+
## What it counts, and how
|
|
59
|
+
|
|
60
|
+
| Model family | How tokens are counted | Label |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| OpenAI GPT-5.x, gpt-oss | Offline with `o200k_base` (the encoding OpenAI's tiktoken maps them to) | exact |
|
|
63
|
+
| OpenAI GPT-6.x | Offline with `o200k_base`; OpenAI has not published a newer encoding | estimate |
|
|
64
|
+
| Claude | Only Anthropic's `count_tokens` API, with your key and `--claude-api` | API count |
|
|
65
|
+
| Gemini | Only Google's `countTokens` API, with your key and `--gemini-api` | API count |
|
|
66
|
+
|
|
67
|
+
Claude's tokenizer is not public, and Anthropic's docs note that newer Claude models produce noticeably more tokens than older ones for the same text. `turntax` never shows an OpenAI count as a Claude or Gemini number: without a key those rows say "needs API key". The `o200k` column is a labelled reference count for every file.
|
|
68
|
+
|
|
69
|
+
Cost per N turns = tokens per turn × N × input price. The "cached" column assumes every turn reads the context from the prompt cache. Prices come from a dated table with a source link per model (`turntax --list-models`); pass `--prices my-prices.json` to use your own.
|
|
70
|
+
|
|
71
|
+
See [docs/how-it-counts.md](docs/how-it-counts.md) for the loading rules per agent and their sources.
|
|
72
|
+
|
|
73
|
+
## Advice
|
|
74
|
+
|
|
75
|
+
All checks are deterministic. Each one shows an estimated saving in o200k tokens per turn.
|
|
76
|
+
|
|
77
|
+
- **duplicate**: identical lines in two files. A per-turn saving only when one tool loads both; otherwise flagged as drift risk.
|
|
78
|
+
- **contradiction**: conflicting rules reaching the same agent (package manager, indentation, quotes, semicolons).
|
|
79
|
+
- **scopable-section**: a section mostly about one directory that could load only there (nested file, path-scoped rule, `applyTo`).
|
|
80
|
+
- **oversized-section**: a large always-loaded section that could become an on-demand skill.
|
|
81
|
+
- **size-limit**: documented limits (CLAUDE.md under 200 lines, Codex's 32 KiB cut-off, Windsurf's 12,000 characters, Cursor's 500 lines).
|
|
82
|
+
- **misconfigured**: files a tool ignores, such as `.md` files in `.cursor/rules` or Windsurf rules without a `trigger`.
|
|
83
|
+
- **stale-reference**: referenced files or `npm run` scripts that no longer exist.
|
|
84
|
+
- **non-english**: non-English sections. The token multiplier is measured only against an English version you supply (`--english CLAUDE.md=CLAUDE.en.md`) or, if you opt in, a Claude translation (`--translate`).
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
turntax [path]
|
|
90
|
+
--tools <ids> agents to report (default: every agent that reads a file); see --help
|
|
91
|
+
--models <ids> see --list-models (default: claude-sonnet-5.5, gpt-6.1-sol, gemini-3.8-flash)
|
|
92
|
+
--turns <n> cost projection (default 100)
|
|
93
|
+
--json | --md | --badge output format (default: terminal table)
|
|
94
|
+
--out <file> write the output to a file
|
|
95
|
+
--max-tokens <n> exit 1 when any tool loads more than n o200k tokens per turn (CI gate)
|
|
96
|
+
--claude-api exact Claude counts (ANTHROPIC_API_KEY)
|
|
97
|
+
--gemini-api Gemini counts (GEMINI_API_KEY)
|
|
98
|
+
--english <file=en> measure a language multiplier against your English version
|
|
99
|
+
--translate opt-in Claude translation for that measurement (needs --claude-api)
|
|
100
|
+
--prices <file.json> your own price table
|
|
101
|
+
--section-tokens <n> threshold for oversized sections (default 600)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Privacy
|
|
105
|
+
|
|
106
|
+
Offline by default: no network calls, no telemetry. File contents are sent only when you pass `--claude-api`, `--gemini-api` or `--translate`, and only to that vendor's API. Keys are read from environment variables and sent only in request headers.
|
|
107
|
+
|
|
108
|
+
## Web page
|
|
109
|
+
|
|
110
|
+
[`web/`](web) is a static page that runs the same engine in the browser with the offline tokenizer only. Drop a repository folder or paste files; nothing is uploaded.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm run build:web
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Badge
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx turntax --badge
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
[-297%20tokens%2Fturn-brightgreen)](https://github.com/TorutDev/turntax)
|
|
123
|
+
|
|
124
|
+
The badge shows the largest always-loaded total among the tools found, counted with o200k_base.
|
|
125
|
+
|
|
126
|
+
## Limits
|
|
127
|
+
|
|
128
|
+
- Assumes the agent starts at the repository root. Files outside the repository (user-level `~/.claude/CLAUDE.md`, global rules, team rules in a dashboard) are not counted.
|
|
129
|
+
- Counts instruction files only, not the agent's own system prompt, tool definitions or MCP servers.
|
|
130
|
+
- Loading rules change. Each rule cites the vendor page and the date it was checked; please open an issue when one is out of date.
|
|
131
|
+
|
|
132
|
+
## Contributing
|
|
133
|
+
|
|
134
|
+
Loading rules and prices go out of date. Corrections with a link to the vendor's page are the most useful contribution; see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
package/dist/core/advice.js
CHANGED
|
@@ -396,7 +396,7 @@ function nonEnglish(units, english) {
|
|
|
396
396
|
/** True for instruction files that live in a package subdirectory rather than at the root or in a root tool folder. */
|
|
397
397
|
function isNested(path) {
|
|
398
398
|
const dir = dirname(path);
|
|
399
|
-
return dir !== '' && !/^\.(claude|github|cursor|windsurf|devin|agents)(\/|$)/.test(dir);
|
|
399
|
+
return dir !== '' && !/^\.(claude|github|cursor|windsurf|devin|agents|clinerules|cline|roo|kiro|junie|continue)(\/|$)/.test(dir);
|
|
400
400
|
}
|
|
401
401
|
function staleReferences(units, opts) {
|
|
402
402
|
const out = [];
|
package/dist/core/classify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename, dirname, findImports, firstParagraph, parseFrontmatter, resolvePath, stripHtmlComments } from './markdown.js';
|
|
1
|
+
import { basename, configList, dirname, findImports, firstParagraph, parseFrontmatter, resolvePath, stripHtmlComments } from './markdown.js';
|
|
2
2
|
import { TOOLS } from './tools.js';
|
|
3
3
|
/** Works out which files each tool loads, when, and with what text. */
|
|
4
4
|
export function classify(input, toolIds) {
|
|
@@ -48,6 +48,28 @@ function applyRule(tool, rule, files, byPath) {
|
|
|
48
48
|
return files
|
|
49
49
|
.filter((f) => rule.pattern.test(f.path))
|
|
50
50
|
.map((f) => unit(tool.id, f.path, 'ignored', rule.reason, f.text, { problem: rule.reason }));
|
|
51
|
+
case 'pattern':
|
|
52
|
+
return files
|
|
53
|
+
.filter((f) => rule.pattern.test(f.path))
|
|
54
|
+
.map((f) => unit(tool.id, f.path, rule.mode, rule.trigger, f.text, { notes: rule.note ? [rule.note] : [] }));
|
|
55
|
+
case 'precedence': {
|
|
56
|
+
const inGroup = (g) => files.filter((f) => g.patterns.some((p) => p.test(f.path)));
|
|
57
|
+
const winner = rule.groups.find((g) => inGroup(g).length > 0);
|
|
58
|
+
if (!winner)
|
|
59
|
+
return [];
|
|
60
|
+
return rule.groups.flatMap((g) => inGroup(g).map((f) => g === winner
|
|
61
|
+
? unit(tool.id, f.path, 'always', 'project instructions', f.text, { notes: rule.note ? [rule.note] : [] })
|
|
62
|
+
: unit(tool.id, f.path, 'ignored', `${winner.label} takes precedence`, f.text)));
|
|
63
|
+
}
|
|
64
|
+
case 'config-list': {
|
|
65
|
+
const config = byPath.get(rule.configPath);
|
|
66
|
+
if (!config)
|
|
67
|
+
return [];
|
|
68
|
+
return configList(config, rule.key)
|
|
69
|
+
.map((p) => byPath.get(p))
|
|
70
|
+
.filter((f) => f !== undefined)
|
|
71
|
+
.map((f) => unit(tool.id, f.path, 'always', rule.trigger, f.text));
|
|
72
|
+
}
|
|
51
73
|
case 'frontmatter':
|
|
52
74
|
return files
|
|
53
75
|
.filter((f) => rule.pattern.test(f.path))
|
|
@@ -111,19 +133,27 @@ function applyHierarchical(tool, rule, files, byPath) {
|
|
|
111
133
|
conditionFails = 'CLAUDE.md exists at the root, so Claude Code reads it instead of AGENTS.md (default setting).';
|
|
112
134
|
if (rule.condition === 'copilot-no-agents-md' && files.some((f) => f.path === 'AGENTS.md'))
|
|
113
135
|
conditionFails = 'A root AGENTS.md exists, which Copilot uses instead.';
|
|
114
|
-
|
|
136
|
+
// Every matching file is read, except where the tool reads only one file per directory.
|
|
137
|
+
const firstPerLevel = new Map();
|
|
138
|
+
const read = [];
|
|
115
139
|
const out = [];
|
|
116
140
|
for (const name of rule.names) {
|
|
117
141
|
for (const f of matches.filter((m) => basename(m.path) === name)) {
|
|
118
142
|
const level = levelOf(f.path, rule.sameLevelDirs);
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
// The docs do not say when subdirectory copies load, so they are not counted either way.
|
|
144
|
+
if (level !== '' && rule.nested === 'skip')
|
|
145
|
+
continue;
|
|
146
|
+
const first = firstPerLevel.get(level);
|
|
147
|
+
if (rule.onePerDirectory && first) {
|
|
148
|
+
out.push(unit(tool.id, f.path, 'ignored', `${basename(first.path)} takes precedence`, f.text));
|
|
121
149
|
continue;
|
|
122
150
|
}
|
|
123
|
-
|
|
151
|
+
if (!first)
|
|
152
|
+
firstPerLevel.set(level, f);
|
|
153
|
+
read.push(f);
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
|
-
for (const f of
|
|
156
|
+
for (const f of read) {
|
|
127
157
|
if (conditionFails) {
|
|
128
158
|
out.push(unit(tool.id, f.path, 'ignored', conditionFails, f.text));
|
|
129
159
|
continue;
|
|
@@ -186,7 +216,18 @@ function expandImports(tool, parent, rule, byPath, depth, seen = new Set([parent
|
|
|
186
216
|
continue;
|
|
187
217
|
seen.add(resolved);
|
|
188
218
|
const notes = [];
|
|
189
|
-
|
|
219
|
+
let mode = parent.mode;
|
|
220
|
+
let trigger = parent.trigger;
|
|
221
|
+
let text = f.text;
|
|
222
|
+
if (rule.importGlobs) {
|
|
223
|
+
const { frontmatter, body } = parseFrontmatter(f.text);
|
|
224
|
+
if (frontmatter.globs !== undefined && String(frontmatter.globs) !== '') {
|
|
225
|
+
mode = 'on-demand';
|
|
226
|
+
trigger = `globs: ${Array.isArray(frontmatter.globs) ? frontmatter.globs.join(', ') : String(frontmatter.globs)}`;
|
|
227
|
+
text = body;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const child = unit(tool, f.path, mode, trigger, transform(text, rule, notes), {
|
|
190
231
|
importedFrom: parent.path,
|
|
191
232
|
notes,
|
|
192
233
|
});
|
package/dist/core/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Which repository paths can hold agent instructions. Shared by the CLI and the web page. */
|
|
2
|
-
import { findImports, resolvePath } from './markdown.js';
|
|
2
|
+
import { configList, findImports, resolvePath } from './markdown.js';
|
|
3
3
|
export const SKIP_DIRS = new Set([
|
|
4
4
|
'node_modules',
|
|
5
5
|
'.git',
|
|
@@ -30,6 +30,13 @@ const NAMES = new Set([
|
|
|
30
30
|
'GEMINI.md',
|
|
31
31
|
'.cursorrules',
|
|
32
32
|
'.windsurfrules',
|
|
33
|
+
'.clinerules',
|
|
34
|
+
'.roorules',
|
|
35
|
+
'.rules',
|
|
36
|
+
'AGENT.md',
|
|
37
|
+
'CONVENTIONS.md',
|
|
38
|
+
'.goosehints',
|
|
39
|
+
'.aider.conf.yml',
|
|
33
40
|
]);
|
|
34
41
|
const PATTERNS = [
|
|
35
42
|
/^\.github\/copilot-instructions\.md$/,
|
|
@@ -40,6 +47,13 @@ const PATTERNS = [
|
|
|
40
47
|
/(^|\/)\.agents\/skills\/[^/]+\/SKILL\.md$/,
|
|
41
48
|
/(^|\/)\.cursor\/rules\/.+\.mdc?$/,
|
|
42
49
|
/(^|\/)\.(devin|windsurf)\/rules\/.+\.md$/,
|
|
50
|
+
/^\.(clinerules|cline\/rules)\/.+\.md$/,
|
|
51
|
+
/^\.roo\/rules(-[\w-]+)?\/.+\.(md|txt)$/,
|
|
52
|
+
/^\.roorules-[\w-]+$/,
|
|
53
|
+
/^\.kiro\/steering\/[^/]+\.md$/,
|
|
54
|
+
/^\.junie\/(AGENTS|playbook|guidelines)\.md$/,
|
|
55
|
+
/^\.junie\/(rules|guidelines)\/[^/]+\.md$/,
|
|
56
|
+
/^\.continue\/rules\/[^/]+\.md$/,
|
|
43
57
|
];
|
|
44
58
|
export const MAX_DEPTH = 12;
|
|
45
59
|
export function isInstructionFile(path) {
|
|
@@ -51,15 +65,22 @@ export function isSkippedDir(path) {
|
|
|
51
65
|
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
52
66
|
return SKIP_DIRS.has(name) || path === '.claude/worktrees' || path.endsWith('/.claude/worktrees');
|
|
53
67
|
}
|
|
54
|
-
/**
|
|
68
|
+
/** Files a file pulls in: its @imports, or the read: list of .aider.conf.yml. */
|
|
69
|
+
function references(f) {
|
|
70
|
+
if (f.path === '.aider.conf.yml')
|
|
71
|
+
return configList(f, 'read');
|
|
72
|
+
return findImports(f.text)
|
|
73
|
+
.map((t) => resolvePath(f.path, t))
|
|
74
|
+
.filter((p) => p !== null);
|
|
75
|
+
}
|
|
76
|
+
/** Adds in-repo files reached through @imports or config read lists (up to five levels) to `found`. */
|
|
55
77
|
export async function addImports(found, read) {
|
|
56
78
|
let queue = [...found.values()];
|
|
57
79
|
for (let depth = 0; depth < 5 && queue.length; depth++) {
|
|
58
80
|
const next = [];
|
|
59
81
|
for (const f of queue) {
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
if (rel === null || found.has(rel))
|
|
82
|
+
for (const rel of references(f)) {
|
|
83
|
+
if (found.has(rel))
|
|
63
84
|
continue;
|
|
64
85
|
const text = await read(rel);
|
|
65
86
|
if (text === null)
|
package/dist/core/markdown.js
CHANGED
|
@@ -168,3 +168,13 @@ export function resolvePath(fromFile, target) {
|
|
|
168
168
|
}
|
|
169
169
|
return parts.join('/');
|
|
170
170
|
}
|
|
171
|
+
/** Paths listed under `key` in a small YAML config (string, inline list or block list). */
|
|
172
|
+
export function configList(config, key) {
|
|
173
|
+
const value = parseFrontmatter(`---\n${config.text}\n---\n`).frontmatter[key];
|
|
174
|
+
const list = value === undefined || value === '' ? [] : Array.isArray(value) ? value : [String(value)];
|
|
175
|
+
return list
|
|
176
|
+
.map((p) => p.replace(/\s+#.*$/, '').trim())
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.map((p) => resolvePath(config.path, p))
|
|
179
|
+
.filter((p) => p !== null);
|
|
180
|
+
}
|
package/dist/core/render.js
CHANGED
|
@@ -21,6 +21,39 @@ function table(rows, indent = '') {
|
|
|
21
21
|
const widths = rows[0].map((_, c) => Math.max(...rows.map((r) => (r[c] ?? '').length)));
|
|
22
22
|
return rows.map((r) => indent + r.map((cell, c) => cell.padEnd(widths[c])).join(' ').trimEnd()).join('\n');
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Agents that load exactly the same files with the same results share one row,
|
|
26
|
+
* so a repository with only AGENTS.md does not print a dozen identical lines.
|
|
27
|
+
*/
|
|
28
|
+
export function groupTools(report) {
|
|
29
|
+
const groups = new Map();
|
|
30
|
+
for (const t of report.tools) {
|
|
31
|
+
const files = report.files
|
|
32
|
+
.filter((f) => f.tool === t.tool && f.mode !== 'ignored')
|
|
33
|
+
.map((f) => [f.path, f.part, f.mode, f.o200k])
|
|
34
|
+
.sort();
|
|
35
|
+
const key = JSON.stringify([files, t.models.map(({ model, perTurn, method, reason }) => [model, perTurn, method, reason])]);
|
|
36
|
+
const g = groups.get(key);
|
|
37
|
+
if (g)
|
|
38
|
+
g.names.push(t.name);
|
|
39
|
+
else
|
|
40
|
+
groups.set(key, { names: [t.name], tool: t });
|
|
41
|
+
}
|
|
42
|
+
return [...groups.values()];
|
|
43
|
+
}
|
|
44
|
+
/** File rows that are identical across agents, merged with the list of agents. */
|
|
45
|
+
export function groupFiles(report) {
|
|
46
|
+
const groups = new Map();
|
|
47
|
+
for (const f of report.files) {
|
|
48
|
+
const key = JSON.stringify([f.path, f.part, f.mode, f.trigger, f.o200k, f.importedFrom ?? '']);
|
|
49
|
+
const g = groups.get(key);
|
|
50
|
+
if (g)
|
|
51
|
+
g.tools.push(f.tool);
|
|
52
|
+
else
|
|
53
|
+
groups.set(key, { file: f, tools: [f.tool] });
|
|
54
|
+
}
|
|
55
|
+
return [...groups.values()];
|
|
56
|
+
}
|
|
24
57
|
/** Highest always-loaded o200k total across the reported tools. */
|
|
25
58
|
export function headlineTokens(report) {
|
|
26
59
|
return Math.max(0, ...report.tools.map((t) => t.perTurnO200k));
|
|
@@ -34,28 +67,30 @@ export function renderText(report, root) {
|
|
|
34
67
|
return out.join('\n');
|
|
35
68
|
}
|
|
36
69
|
const rows = [
|
|
37
|
-
['
|
|
70
|
+
['always', 'o200k/turn', 'on-demand', 'model', 'tokens/turn', 'counted by', `${report.turns} turns`, 'cached', 'agents'],
|
|
38
71
|
];
|
|
39
|
-
for (const t of report
|
|
40
|
-
const head = [
|
|
72
|
+
for (const { names, tool: t } of groupTools(report)) {
|
|
73
|
+
const head = [String(t.alwaysFiles), fmtTokens(t.perTurnO200k), fmtTokens(t.onDemandO200k)];
|
|
41
74
|
const blank = head.map(() => '');
|
|
75
|
+
const agents = names.join(', ');
|
|
42
76
|
if (!t.models.length)
|
|
43
|
-
rows.push([...head, '(no selected model)', '', '', '', '']);
|
|
77
|
+
rows.push([...head, '(no selected model)', '', '', '', '', agents]);
|
|
44
78
|
t.models.forEach((m, i) => rows.push([
|
|
45
79
|
...(i === 0 ? head : blank),
|
|
46
80
|
m.label,
|
|
47
81
|
...(m.perTurn === null
|
|
48
82
|
? ['-', m.reason ?? METHOD_LABEL[m.method], '-', '-']
|
|
49
83
|
: [fmtTokens(m.perTurn), METHOD_LABEL[m.method], fmtUsd(m.uncached), fmtUsd(m.cached)]),
|
|
84
|
+
i === 0 ? agents : '',
|
|
50
85
|
]));
|
|
51
86
|
}
|
|
52
87
|
out.push(table(rows));
|
|
53
88
|
out.push('');
|
|
54
89
|
out.push('Files');
|
|
55
|
-
const fileRows = [['
|
|
56
|
-
for (const f of report
|
|
90
|
+
const fileRows = [['mode', 'file', 'o200k', 'loads', 'agents']];
|
|
91
|
+
for (const { file: f, tools } of groupFiles(report)) {
|
|
57
92
|
const name = f.part === 'full' ? f.path : `${f.path} (${f.part})`;
|
|
58
|
-
fileRows.push([f.
|
|
93
|
+
fileRows.push([f.mode, name + (f.importedFrom ? ` <- @${f.importedFrom}` : ''), fmtTokens(f.o200k), f.trigger, tools.join(', ')]);
|
|
59
94
|
}
|
|
60
95
|
out.push(table(fileRows, ' '));
|
|
61
96
|
const notes = [...new Set(report.files.flatMap((f) => f.notes.map((n) => `${f.path}: ${n}`)))];
|
|
@@ -95,20 +130,21 @@ function footer(report) {
|
|
|
95
130
|
export function renderMarkdown(report) {
|
|
96
131
|
const out = ['# Agent context tax report', ''];
|
|
97
132
|
out.push(`Every agent re-sends its always-loaded instruction files with each turn. Costs are for ${report.turns} turns.`, '');
|
|
98
|
-
out.push('## Per
|
|
99
|
-
out.push('|
|
|
100
|
-
for (const t of report
|
|
133
|
+
out.push('## Per agent', '');
|
|
134
|
+
out.push('| Agents | Model | Tokens / turn | Counted by | Uncached | Cached |', '| --- | --- | ---: | --- | ---: | ---: |');
|
|
135
|
+
for (const { names, tool: t } of groupTools(report)) {
|
|
136
|
+
const agents = names.join(', ');
|
|
101
137
|
if (!t.models.length)
|
|
102
|
-
out.push(`| ${
|
|
138
|
+
out.push(`| ${agents} | - | ${fmtTokens(t.perTurnO200k)} (o200k) | reference | - | - |`);
|
|
103
139
|
for (const m of t.models)
|
|
104
140
|
out.push(m.perTurn === null
|
|
105
|
-
? `| ${
|
|
106
|
-
: `| ${
|
|
141
|
+
? `| ${agents} | ${m.label} | - | ${m.reason ?? ''} | - | - |`
|
|
142
|
+
: `| ${agents} | ${m.label} | ${fmtTokens(m.perTurn)} | ${METHOD_LABEL[m.method]} | ${fmtUsd(m.uncached)} | ${fmtUsd(m.cached)} |`);
|
|
107
143
|
}
|
|
108
|
-
out.push('', '## Files', '', '|
|
|
109
|
-
for (const f of report
|
|
144
|
+
out.push('', '## Files', '', '| Mode | File | o200k tokens | Loads | Agents |', '| --- | --- | ---: | --- | --- |');
|
|
145
|
+
for (const { file: f, tools } of groupFiles(report)) {
|
|
110
146
|
const name = f.part === 'full' ? f.path : `${f.path} (${f.part})`;
|
|
111
|
-
out.push(`| ${f.
|
|
147
|
+
out.push(`| ${f.mode} | \`${name}\` | ${fmtTokens(f.o200k)} | ${escapeCell(f.trigger)} | ${tools.join(', ')} |`);
|
|
112
148
|
}
|
|
113
149
|
out.push('', '## Advice', '');
|
|
114
150
|
if (!report.advice.length)
|
package/dist/core/tools.js
CHANGED
|
@@ -291,6 +291,256 @@ export const TOOLS = [
|
|
|
291
291
|
],
|
|
292
292
|
notes: ['Global rules (~/.codeium/windsurf/memories/global_rules.md) are outside the repository and not counted.'],
|
|
293
293
|
},
|
|
294
|
+
{
|
|
295
|
+
id: 'cline',
|
|
296
|
+
name: 'Cline',
|
|
297
|
+
families: [],
|
|
298
|
+
sources: ['https://docs.cline.bot/features/cline-rules'],
|
|
299
|
+
checkedAt: '2026-10-10',
|
|
300
|
+
rules: [
|
|
301
|
+
{
|
|
302
|
+
kind: 'frontmatter',
|
|
303
|
+
pattern: /^\.(?:clinerules|cline\/rules)\/.+\.md$/,
|
|
304
|
+
rootPrefixes: ['.clinerules/', '.cline/rules/'],
|
|
305
|
+
decide: (fm) => {
|
|
306
|
+
if (Array.isArray(fm.paths) && fm.paths.length === 0)
|
|
307
|
+
return { mode: 'ignored', trigger: 'paths: [] (never applies)' };
|
|
308
|
+
if (has(fm, 'paths'))
|
|
309
|
+
return { mode: 'on-demand', trigger: `paths: ${str(fm.paths)}` };
|
|
310
|
+
return { mode: 'always', trigger: 'rule without paths' };
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
limits: [],
|
|
315
|
+
notes: [
|
|
316
|
+
'Each rule can be toggled off in Cline; the audit assumes rules are on.',
|
|
317
|
+
'Cline also detects AGENTS.md, .cursorrules and .windsurfrules; whether they are on by default is not documented, so they are not counted.',
|
|
318
|
+
'A single .clinerules file (instead of a folder) is not documented and is not counted.',
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: 'roo-code',
|
|
323
|
+
name: 'Roo Code',
|
|
324
|
+
families: [],
|
|
325
|
+
sources: ['https://roocodeinc.github.io/Roo-Code/features/custom-instructions'],
|
|
326
|
+
checkedAt: '2026-10-10',
|
|
327
|
+
rules: [
|
|
328
|
+
{
|
|
329
|
+
kind: 'precedence',
|
|
330
|
+
groups: [
|
|
331
|
+
{ label: '.roo/rules/', patterns: [/^\.roo\/rules\/.+/] },
|
|
332
|
+
{ label: '.roorules', patterns: [/^\.roorules$/] },
|
|
333
|
+
{ label: '.clinerules', patterns: [/^\.clinerules$/] },
|
|
334
|
+
],
|
|
335
|
+
note: 'Legacy .roorules and .clinerules are read only when .roo/rules/ has no content; their order between each other is assumed.',
|
|
336
|
+
},
|
|
337
|
+
{ kind: 'pattern', pattern: /^\.roo\/rules-[\w-]+\/.+/, mode: 'on-demand', trigger: 'only in that Roo mode' },
|
|
338
|
+
{ kind: 'pattern', pattern: /^\.roorules-[\w-]+$/, mode: 'on-demand', trigger: 'only in that Roo mode (legacy)' },
|
|
339
|
+
{
|
|
340
|
+
kind: 'hierarchical',
|
|
341
|
+
names: ['AGENTS.md', 'AGENT.md'],
|
|
342
|
+
onePerDirectory: true,
|
|
343
|
+
nested: 'skip',
|
|
344
|
+
nestedTrigger: '',
|
|
345
|
+
note: 'Read by default; the roo-cline.useAgentRules setting turns it off.',
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
limits: [],
|
|
349
|
+
notes: ['Global ~/.roo/rules/ is outside the repository and not counted.'],
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 'kiro',
|
|
353
|
+
name: 'Kiro',
|
|
354
|
+
families: [],
|
|
355
|
+
sources: ['https://kiro.dev/docs/steering/'],
|
|
356
|
+
checkedAt: '2026-10-10',
|
|
357
|
+
rules: [
|
|
358
|
+
{
|
|
359
|
+
kind: 'frontmatter',
|
|
360
|
+
pattern: /^\.kiro\/steering\/[^/]+\.md$/,
|
|
361
|
+
rootPrefixes: ['.kiro/steering/'],
|
|
362
|
+
decide: (fm) => {
|
|
363
|
+
switch (str(fm.inclusion).trim()) {
|
|
364
|
+
case '':
|
|
365
|
+
case 'always':
|
|
366
|
+
return { mode: 'always', trigger: 'inclusion: always' };
|
|
367
|
+
case 'fileMatch':
|
|
368
|
+
return { mode: 'on-demand', trigger: `fileMatch: ${str(fm.fileMatchPattern)}` };
|
|
369
|
+
case 'manual':
|
|
370
|
+
return { mode: 'manual', trigger: 'inclusion: manual (#name)' };
|
|
371
|
+
case 'auto':
|
|
372
|
+
return {
|
|
373
|
+
mode: 'on-demand',
|
|
374
|
+
trigger: 'inclusion: auto (matched on description)',
|
|
375
|
+
descriptionOnly: true,
|
|
376
|
+
note: 'Description assumed to be in context so Kiro can match it (inferred, not documented).',
|
|
377
|
+
};
|
|
378
|
+
default:
|
|
379
|
+
return { mode: 'always', trigger: `inclusion: ${str(fm.inclusion)} (unknown, treated as always)` };
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{ kind: 'hierarchical', names: ['AGENTS.md'], nested: 'skip', nestedTrigger: '' },
|
|
384
|
+
],
|
|
385
|
+
limits: [],
|
|
386
|
+
notes: [
|
|
387
|
+
'fileMatch and manual modes need Kiro CLI V3; older versions skip those files.',
|
|
388
|
+
'Global ~/.kiro/steering/ is outside the repository and not counted.',
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 'junie',
|
|
393
|
+
name: 'JetBrains Junie',
|
|
394
|
+
families: [],
|
|
395
|
+
sources: ['https://junie.jetbrains.com/docs/guidelines-and-memory.html'],
|
|
396
|
+
checkedAt: '2026-10-10',
|
|
397
|
+
rules: [
|
|
398
|
+
{
|
|
399
|
+
kind: 'precedence',
|
|
400
|
+
groups: [
|
|
401
|
+
{ label: '.junie/AGENTS.md', patterns: [/^\.junie\/AGENTS\.md$/] },
|
|
402
|
+
{ label: 'AGENTS.md', patterns: [/^AGENTS\.md$/, /^\.junie\/playbook\.md$/, /^\.junie\/rules\/[^/]+\.md$/] },
|
|
403
|
+
{ label: '.junie/guidelines', patterns: [/^\.junie\/guidelines\.md$/, /^\.junie\/guidelines\/[^/]+\.md$/] },
|
|
404
|
+
],
|
|
405
|
+
note: 'Follows the IDE plugin order; a custom guidelines path set in project settings is not modelled.',
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
limits: [],
|
|
409
|
+
notes: ['Global ~/.junie/AGENTS.md is outside the repository and not counted.'],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: 'zed',
|
|
413
|
+
name: 'Zed',
|
|
414
|
+
families: [],
|
|
415
|
+
sources: ['https://zed.dev/docs/ai/instructions'],
|
|
416
|
+
checkedAt: '2026-10-10',
|
|
417
|
+
rules: [
|
|
418
|
+
{
|
|
419
|
+
kind: 'precedence',
|
|
420
|
+
groups: [
|
|
421
|
+
'.rules',
|
|
422
|
+
'.cursorrules',
|
|
423
|
+
'.windsurfrules',
|
|
424
|
+
'.clinerules',
|
|
425
|
+
'.github/copilot-instructions.md',
|
|
426
|
+
'AGENT.md',
|
|
427
|
+
'AGENTS.md',
|
|
428
|
+
'CLAUDE.md',
|
|
429
|
+
'GEMINI.md',
|
|
430
|
+
].map((path) => ({ label: path, patterns: [new RegExp(`^${path.replace(/\./g, '\\.')}$`)] })),
|
|
431
|
+
note: 'Zed uses only the first of these files it finds at the project root.',
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
limits: [],
|
|
435
|
+
notes: ['Personal ~/.config/zed/AGENTS.md is outside the repository and not counted.'],
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: 'continue',
|
|
439
|
+
name: 'Continue',
|
|
440
|
+
families: [],
|
|
441
|
+
sources: ['https://docs.continue.dev/customize/deep-dives/rules'],
|
|
442
|
+
checkedAt: '2026-10-10',
|
|
443
|
+
rules: [
|
|
444
|
+
{
|
|
445
|
+
kind: 'frontmatter',
|
|
446
|
+
pattern: /^\.continue\/rules\/[^/]+\.md$/,
|
|
447
|
+
rootPrefixes: ['.continue/rules/'],
|
|
448
|
+
decide: (fm) => {
|
|
449
|
+
const always = fm.alwaysApply === true || str(fm.alwaysApply) === 'true';
|
|
450
|
+
const never = fm.alwaysApply === false || str(fm.alwaysApply) === 'false';
|
|
451
|
+
if (always)
|
|
452
|
+
return { mode: 'always', trigger: 'alwaysApply: true' };
|
|
453
|
+
if (has(fm, 'globs'))
|
|
454
|
+
return { mode: 'on-demand', trigger: `globs: ${str(fm.globs)}` };
|
|
455
|
+
if (!never)
|
|
456
|
+
return { mode: 'always', trigger: 'no globs' };
|
|
457
|
+
if (has(fm, 'description'))
|
|
458
|
+
return {
|
|
459
|
+
mode: 'on-demand',
|
|
460
|
+
trigger: 'agent decides from description',
|
|
461
|
+
descriptionOnly: true,
|
|
462
|
+
note: 'Description assumed to be in context so the agent can decide (inferred, not documented).',
|
|
463
|
+
};
|
|
464
|
+
return { mode: 'on-demand', trigger: 'alwaysApply: false (agent decides)' };
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
limits: [],
|
|
469
|
+
notes: ['Rules in config.yaml or from the Continue hub are not counted.'],
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: 'aider',
|
|
473
|
+
name: 'Aider',
|
|
474
|
+
families: [],
|
|
475
|
+
sources: ['https://aider.chat/docs/usage/conventions.html'],
|
|
476
|
+
checkedAt: '2026-10-10',
|
|
477
|
+
rules: [
|
|
478
|
+
{ kind: 'config-list', configPath: '.aider.conf.yml', key: 'read', trigger: 'read: in .aider.conf.yml' },
|
|
479
|
+
{ kind: 'pattern', pattern: /^CONVENTIONS\.md$/, mode: 'manual', trigger: 'only with --read, /read or read: in .aider.conf.yml' },
|
|
480
|
+
],
|
|
481
|
+
limits: [],
|
|
482
|
+
notes: ['Aider loads no instruction file by itself; files given with --read are sent with every request.'],
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'goose',
|
|
486
|
+
name: 'Goose',
|
|
487
|
+
families: [],
|
|
488
|
+
sources: ['https://goose-docs.ai/docs/guides/context-engineering/using-goosehints'],
|
|
489
|
+
checkedAt: '2026-10-10',
|
|
490
|
+
rules: [
|
|
491
|
+
{
|
|
492
|
+
kind: 'hierarchical',
|
|
493
|
+
names: ['AGENTS.md', '.goosehints'],
|
|
494
|
+
nested: 'on-demand',
|
|
495
|
+
nestedTrigger: 'when goose accesses files in',
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
limits: [],
|
|
499
|
+
notes: [
|
|
500
|
+
'Needs the Developer extension; CONTEXT_FILE_NAMES can change which files are read.',
|
|
501
|
+
'Global ~/.config/goose/.goosehints is outside the repository and not counted.',
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
id: 'amp',
|
|
506
|
+
name: 'Amp',
|
|
507
|
+
families: [],
|
|
508
|
+
sources: ['https://ampcode.com/docs/customize/agents-md'],
|
|
509
|
+
checkedAt: '2026-10-10',
|
|
510
|
+
rules: [
|
|
511
|
+
{
|
|
512
|
+
kind: 'hierarchical',
|
|
513
|
+
names: ['AGENTS.md', 'AGENT.md', 'CLAUDE.md'],
|
|
514
|
+
onePerDirectory: true,
|
|
515
|
+
nested: 'on-demand',
|
|
516
|
+
nestedTrigger: 'when Amp reads a file in',
|
|
517
|
+
importDepth: 1,
|
|
518
|
+
importGlobs: true,
|
|
519
|
+
note: 'AGENT.md and CLAUDE.md are fallbacks for directories without AGENTS.md; their order between each other, and following @-mentions one level deep, are assumptions.',
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
limits: [],
|
|
523
|
+
notes: ['User and system AGENTS.md files (~/.config/amp, /etc/ampcode) are outside the repository and not counted.'],
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: 'opencode',
|
|
527
|
+
name: 'OpenCode',
|
|
528
|
+
families: [],
|
|
529
|
+
sources: ['https://opencode.ai/docs/rules/'],
|
|
530
|
+
checkedAt: '2026-10-10',
|
|
531
|
+
rules: [
|
|
532
|
+
{
|
|
533
|
+
kind: 'hierarchical',
|
|
534
|
+
names: ['AGENTS.md', 'CLAUDE.md'],
|
|
535
|
+
onePerDirectory: true,
|
|
536
|
+
nested: 'skip',
|
|
537
|
+
nestedTrigger: '',
|
|
538
|
+
note: 'CLAUDE.md is read only when there is no AGENTS.md.',
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
limits: [],
|
|
542
|
+
notes: ['Extra files listed under instructions in opencode.json are not counted.'],
|
|
543
|
+
},
|
|
294
544
|
];
|
|
295
545
|
export const TOOL_IDS = TOOLS.map((t) => t.id);
|
|
296
546
|
export function getTool(id) {
|
package/package.json
CHANGED
|
@@ -1,53 +1,63 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "turntax",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Measure the per-turn token
|
|
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
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"codex",
|
|
36
|
-
"cursor",
|
|
37
|
-
"copilot",
|
|
38
|
-
"gemini-cli",
|
|
39
|
-
"windsurf",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "turntax",
|
|
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
|
+
}
|