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.
Files changed (3) hide show
  1. package/README.md +38 -0
  2. package/index.mjs +169 -0
  3. 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
+ }