aitokenweight 0.1.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 +38 -0
- package/index.mjs +169 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# aitokenweight CLI
|
|
2
|
+
|
|
3
|
+
One command from token logs to a shareable poster:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx aitokenweight
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Reads today's exact Claude Code token usage from local transcripts (via
|
|
10
|
+
[ccusage](https://github.com/ryoppippi/ccusage)), fills it into the
|
|
11
|
+
[aitokenweight](https://github.com/susyimes/aitokenweight) poster page, prints
|
|
12
|
+
the filled URL, and opens it in your browser.
|
|
13
|
+
|
|
14
|
+
Strict policy: exact numbers only. If no usage is recorded for today, it prints
|
|
15
|
+
`usage_unavailable` and exits 1 — it never renders estimates or demo values.
|
|
16
|
+
|
|
17
|
+
## Options
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
--handle <name> Name shown on the poster (default: OS username)
|
|
21
|
+
--origin <url> Poster site origin (default: https://susyimes.github.io/aitokenweight/)
|
|
22
|
+
--timezone <iana> Timezone for "today" (default: Asia/Shanghai)
|
|
23
|
+
--date <YYYY-MM-DD> Override the report date
|
|
24
|
+
--wh <number> Wh per 1K tokens (default: 0.4)
|
|
25
|
+
--json Print machine-readable skill result JSON only
|
|
26
|
+
--no-open Do not open the poster URL in a browser
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## For AI agents
|
|
30
|
+
|
|
31
|
+
`npx -y aitokenweight@latest --json --no-open` prints exactly one JSON object:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{"status":"rendered","totalTokens":123,"usageEvidence":"...","posterPath":"https://...","checkedSources":["local_log"]}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
or, when no exact usage exists, `{"status":"usage_unavailable", ...}` with exit
|
|
38
|
+
code 1. See the site's `/agent.md` for the full skill contract.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { userInfo } from 'node:os'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ORIGIN = 'https://susyimes.github.io/aitokenweight/'
|
|
6
|
+
const DEFAULT_TIMEZONE = 'Asia/Shanghai'
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const args = {}
|
|
10
|
+
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const item = argv[index]
|
|
13
|
+
|
|
14
|
+
if (!item.startsWith('--')) continue
|
|
15
|
+
|
|
16
|
+
const key = item.slice(2)
|
|
17
|
+
const next = argv[index + 1]
|
|
18
|
+
|
|
19
|
+
if (!next || next.startsWith('--')) {
|
|
20
|
+
args[key] = true
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
args[key] = next
|
|
25
|
+
index += 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return args
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isoDate(date, timeZone) {
|
|
32
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
33
|
+
timeZone,
|
|
34
|
+
year: 'numeric',
|
|
35
|
+
month: '2-digit',
|
|
36
|
+
day: '2-digit',
|
|
37
|
+
}).format(date)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unavailable(reason, checkedSources) {
|
|
41
|
+
console.log(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
status: 'usage_unavailable',
|
|
44
|
+
totalTokens: null,
|
|
45
|
+
usageEvidence: reason,
|
|
46
|
+
posterPath: null,
|
|
47
|
+
checkedSources,
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function openInBrowser(url) {
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
spawnSync('rundll32', ['url.dll,FileProtocolHandler', url])
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
spawnSync(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], {
|
|
60
|
+
stdio: 'ignore',
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const args = parseArgs(process.argv.slice(2))
|
|
65
|
+
|
|
66
|
+
if (args.help) {
|
|
67
|
+
console.log(`aitokenweight — turn today's AI token usage into a shareable poster URL
|
|
68
|
+
|
|
69
|
+
Usage: npx aitokenweight [options]
|
|
70
|
+
|
|
71
|
+
Reads today's Claude Code token usage from local transcripts (via ccusage)
|
|
72
|
+
and prints a filled poster URL. No estimates: if no exact usage is found,
|
|
73
|
+
prints usage_unavailable and exits 1.
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
--handle <name> Name shown on the poster (default: OS username)
|
|
77
|
+
--origin <url> Poster site origin (default: ${DEFAULT_ORIGIN})
|
|
78
|
+
--timezone <iana> Timezone for "today" (default: ${DEFAULT_TIMEZONE})
|
|
79
|
+
--date <YYYY-MM-DD> Override the report date
|
|
80
|
+
--wh <number> Wh per 1K tokens (default: 0.4)
|
|
81
|
+
--json Print machine-readable skill result JSON only
|
|
82
|
+
--no-open Do not open the poster URL in a browser
|
|
83
|
+
--help Show this help`)
|
|
84
|
+
process.exit(0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const timezone = args.timezone ?? DEFAULT_TIMEZONE
|
|
88
|
+
const date = args.date ?? isoDate(new Date(), timezone)
|
|
89
|
+
const since = isoDate(new Date(Date.now() - 6 * 86_400_000), timezone).replaceAll(
|
|
90
|
+
'-',
|
|
91
|
+
'',
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const result = spawnSync(
|
|
95
|
+
'npx',
|
|
96
|
+
['-y', 'ccusage@latest', 'daily', '--json', '--since', since],
|
|
97
|
+
{
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
shell: process.platform === 'win32',
|
|
100
|
+
windowsHide: true,
|
|
101
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (result.error || result.status !== 0 || !result.stdout) {
|
|
106
|
+
unavailable(
|
|
107
|
+
`ccusage failed: ${result.error?.message ?? result.stderr?.slice(0, 200) ?? 'no output'}`,
|
|
108
|
+
['local_log'],
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let daily
|
|
113
|
+
try {
|
|
114
|
+
const jsonStart = result.stdout.indexOf('{')
|
|
115
|
+
daily = JSON.parse(result.stdout.slice(jsonStart)).daily ?? []
|
|
116
|
+
} catch {
|
|
117
|
+
unavailable('ccusage output was not valid JSON', ['local_log'])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hasAgentField = daily.some((entry) => 'agent' in entry)
|
|
121
|
+
const entries = (hasAgentField
|
|
122
|
+
? daily.filter((entry) => entry.agent === 'all')
|
|
123
|
+
: daily
|
|
124
|
+
).sort((a, b) => a.period.localeCompare(b.period))
|
|
125
|
+
const today = entries.find((entry) => entry.period === date)
|
|
126
|
+
|
|
127
|
+
if (!today || !(today.totalTokens > 0)) {
|
|
128
|
+
unavailable(`no transcript usage recorded for ${date} (${timezone})`, [
|
|
129
|
+
'local_log',
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const usage = {
|
|
134
|
+
date,
|
|
135
|
+
timezone,
|
|
136
|
+
provider: 'agent-runtime',
|
|
137
|
+
handle: args.handle ?? userInfo().username,
|
|
138
|
+
inputTokens: today.inputTokens ?? 0,
|
|
139
|
+
outputTokens: today.outputTokens ?? 0,
|
|
140
|
+
cachedTokens: (today.cacheReadTokens ?? 0) + (today.cacheCreationTokens ?? 0),
|
|
141
|
+
totalTokens: today.totalTokens,
|
|
142
|
+
whPerThousand: Number(args.wh ?? 0.4),
|
|
143
|
+
history: entries.slice(-7).map((entry) => entry.totalTokens),
|
|
144
|
+
source: 'local_log',
|
|
145
|
+
usageEvidence: `ccusage daily --json over local Claude Code transcripts for ${date}`,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let origin = args.origin ?? process.env.AITOKENWEIGHT_ORIGIN ?? DEFAULT_ORIGIN
|
|
149
|
+
if (!origin.endsWith('/')) origin += '/'
|
|
150
|
+
|
|
151
|
+
const encoded = Buffer.from(JSON.stringify(usage), 'utf8').toString('base64url')
|
|
152
|
+
const posterUrl = new URL(`?poster=1&data=${encoded}`, origin).href
|
|
153
|
+
|
|
154
|
+
if (args.json) {
|
|
155
|
+
console.log(
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
status: 'rendered',
|
|
158
|
+
totalTokens: usage.totalTokens,
|
|
159
|
+
usageEvidence: usage.usageEvidence,
|
|
160
|
+
posterPath: posterUrl,
|
|
161
|
+
checkedSources: ['local_log'],
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
} else {
|
|
165
|
+
console.log(`今日 (${date}) Token 消耗:${usage.totalTokens.toLocaleString('en-US')} tokens`)
|
|
166
|
+
console.log(`海报链接:\n${posterUrl}`)
|
|
167
|
+
|
|
168
|
+
if (!args['no-open']) openInBrowser(posterUrl)
|
|
169
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aitokenweight",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Turn today's AI token usage into a shareable aitokenweight poster URL. Reads local Claude Code transcripts via ccusage — exact numbers only, never estimates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aitokenweight": "./index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/susyimes/aitokenweight.git",
|
|
19
|
+
"directory": "cli"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://susyimes.github.io/aitokenweight/",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"claude-code",
|
|
24
|
+
"token-usage",
|
|
25
|
+
"ccusage",
|
|
26
|
+
"poster",
|
|
27
|
+
"agent-skill"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|