cloud-usage-token 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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/codex-hook-bridge.js +95 -0
- package/dist/hook-bridge.js +139 -0
- package/dist/index.js +7470 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos Guilarte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# cloud-usage-token
|
|
2
|
+
|
|
3
|
+
Monitor your **Claude Code** and **OpenAI Codex CLI** usage **live** — sessions, limits, costs, and the status of each agent — from the [CloudUsage](https://cloudusage.space) dashboard, across all your machines.
|
|
4
|
+
|
|
5
|
+
> **Your code stays on your machine.** What the agent sends is session metadata: usage (tokens, costs), quotas, and live status — including a short preview of the last assistant message and a label of the current activity (e.g. the file or command being worked on). Absolute paths, environment variables, tokens/secrets, and emails are **redacted before sending**. Your full file contents and full prompts are never transmitted.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- **Node.js 20 or newer** (`node --version`).
|
|
10
|
+
- A free account at [cloudusage.space](https://cloudusage.space).
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g cloud-usage-token
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Getting started (3 steps)
|
|
19
|
+
|
|
20
|
+
**1. Create your account and a device**
|
|
21
|
+
|
|
22
|
+
Go to [cloudusage.space](https://cloudusage.space), sign up, then open **Settings → API & devices → Create device**. Copy the **API key** (`cut_agent_...`) — it's shown only once.
|
|
23
|
+
|
|
24
|
+
**2. Connect the agent**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cloud-usage-token connect --api-key=cut_agent_YOUR_KEY
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> Prefer not to leave the key in your shell history? Pipe it (`echo cut_agent_YOUR_KEY | cloud-usage-token connect`) or set `CLOUD_USAGE_DEVICE_API_KEY`.
|
|
31
|
+
|
|
32
|
+
**3. Run it in the background**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cloud-usage-token start
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Done. It starts automatically at login and restarts if it crashes. Within a few seconds your device shows up **online** in the dashboard, with your usage and sessions live.
|
|
39
|
+
|
|
40
|
+
## Live status (optional but recommended)
|
|
41
|
+
|
|
42
|
+
For the dashboard to show the **session traffic light** accurately (🟢 working · 🔴 your turn · 🔴 waiting for your approval), install the hooks:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cloud-usage-token install-hooks # for Claude Code
|
|
46
|
+
cloud-usage-token install-hooks --codex # for OpenAI Codex
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Hooks are added **non-destructively** (your existing hooks/config are preserved) and can be removed anytime. With Codex, the first time the CLI will ask you to **trust** the hook — approve it.
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `cloud-usage-token connect --api-key=cut_agent_xxx` | Configure the device (saves the key to `~/.cloud-usage/config.json`). |
|
|
56
|
+
| `cloud-usage-token start` | Run the agent in the background + autostart at login. |
|
|
57
|
+
| `cloud-usage-token status` | Show configuration and service status. |
|
|
58
|
+
| `cloud-usage-token stop` | Stop and remove the service. |
|
|
59
|
+
| `cloud-usage-token install-hooks` | Install the live-status hooks for **Claude Code**. |
|
|
60
|
+
| `cloud-usage-token install-hooks --codex` | Install the hooks for **OpenAI Codex**. |
|
|
61
|
+
| `cloud-usage-token uninstall-hooks` | Remove the Claude hooks (`--codex` for Codex). |
|
|
62
|
+
| `cloud-usage-token help` | Show help. |
|
|
63
|
+
|
|
64
|
+
### `install-hooks` options (Claude)
|
|
65
|
+
|
|
66
|
+
By default it installs the hooks at the user level (global). You can scope it:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cloud-usage-token install-hooks --global # ~/.claude/settings.json (default)
|
|
70
|
+
cloud-usage-token install-hooks --project # ./.claude/settings.json (this project)
|
|
71
|
+
cloud-usage-token install-hooks --settings <path> # a specific settings.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What it monitors
|
|
75
|
+
|
|
76
|
+
- **Usage & costs**: tokens and cost per day, trend, per-model breakdown.
|
|
77
|
+
- **Limits / plan**: quota bars (session, weekly, per model).
|
|
78
|
+
- **Live sessions**: which agents are working, which need your response, activity and context.
|
|
79
|
+
|
|
80
|
+
All of this is computed **locally** on your machine. What travels to the server is metadata — numbers, statuses, and short redacted previews — not your files or full prompts.
|
|
81
|
+
|
|
82
|
+
## Uninstall
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cloud-usage-token stop # stop the service
|
|
86
|
+
cloud-usage-token uninstall-hooks # remove Claude hooks
|
|
87
|
+
cloud-usage-token uninstall-hooks --codex
|
|
88
|
+
npm uninstall -g cloud-usage-token # remove the CLI
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Local config lives in `~/.cloud-usage/config.json` (you can delete it by hand).
|
|
92
|
+
|
|
93
|
+
## Troubleshooting
|
|
94
|
+
|
|
95
|
+
- **Not showing online in the dashboard**: make sure you ran `connect` with the right key and then `start`. Check with `cloud-usage-token status`.
|
|
96
|
+
- **Device was revoked**: if you rotated or deleted the API key in the dashboard, create a new device and run `connect` with the new key.
|
|
97
|
+
- **Codex hooks don't fire**: Codex only runs trusted hooks — the first time, in the interactive TUI, approve the hook when prompted.
|
|
98
|
+
- **Connecting to your own server** (self-host): pass your endpoint with `connect --api-key=... --realtime-url=wss://your-domain/agent`.
|
|
99
|
+
|
|
100
|
+
## Links
|
|
101
|
+
|
|
102
|
+
- Dashboard: [cloudusage.space](https://cloudusage.space)
|
|
103
|
+
- Development / contributing: see [DEVELOPMENT.md](https://github.com/cguilarte/cloud-usage-agent/blob/main/docs/DEVELOPMENT.md)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/codex-hook-bridge.ts
|
|
4
|
+
import http from "http";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
var CONFIG_DIR_NAME = ".cloud-usage";
|
|
11
|
+
var HOOKS_PORT_FILE_NAME = "hooks-port";
|
|
12
|
+
function hooksPortFilePath(home = homedir()) {
|
|
13
|
+
return path.join(home, CONFIG_DIR_NAME, HOOKS_PORT_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
async function readHooksPort(home = homedir()) {
|
|
16
|
+
try {
|
|
17
|
+
const port = Number((await fs.readFile(hooksPortFilePath(home), "utf8")).trim());
|
|
18
|
+
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
19
|
+
} catch {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/codex-hook-bridge-core.ts
|
|
25
|
+
function asRecord(value) {
|
|
26
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
27
|
+
}
|
|
28
|
+
function mapCodexHookToHint(payload, at) {
|
|
29
|
+
const p = asRecord(payload);
|
|
30
|
+
const sessionId = typeof p.session_id === "string" ? p.session_id : void 0;
|
|
31
|
+
if (!sessionId) return null;
|
|
32
|
+
const event = typeof p.hook_event_name === "string" ? p.hook_event_name : "";
|
|
33
|
+
switch (event) {
|
|
34
|
+
case "PermissionRequest":
|
|
35
|
+
return { sessionId, kind: "permission", at };
|
|
36
|
+
case "Stop":
|
|
37
|
+
case "SessionStart":
|
|
38
|
+
return { sessionId, kind: "activity", at };
|
|
39
|
+
default:
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/codex-hook-bridge.ts
|
|
45
|
+
var MAX_STDIN_BYTES = 64 * 1024;
|
|
46
|
+
var POST_TIMEOUT_MS = 200;
|
|
47
|
+
function readStdin() {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
let data = "";
|
|
50
|
+
let size = 0;
|
|
51
|
+
process.stdin.setEncoding("utf8");
|
|
52
|
+
process.stdin.on("data", (chunk) => {
|
|
53
|
+
size += Buffer.byteLength(chunk);
|
|
54
|
+
if (size <= MAX_STDIN_BYTES) data += chunk;
|
|
55
|
+
});
|
|
56
|
+
process.stdin.on("end", () => resolve(data));
|
|
57
|
+
process.stdin.on("error", () => resolve(data));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function post(port, body) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const data = JSON.stringify(body);
|
|
63
|
+
const request = http.request(
|
|
64
|
+
{
|
|
65
|
+
host: "127.0.0.1",
|
|
66
|
+
port,
|
|
67
|
+
path: "/hooks/codex",
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(data) },
|
|
70
|
+
timeout: POST_TIMEOUT_MS
|
|
71
|
+
},
|
|
72
|
+
() => resolve()
|
|
73
|
+
);
|
|
74
|
+
request.on("error", () => resolve());
|
|
75
|
+
request.on("timeout", () => {
|
|
76
|
+
request.destroy();
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
request.end(data);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function main() {
|
|
83
|
+
const raw = await readStdin();
|
|
84
|
+
let payload;
|
|
85
|
+
try {
|
|
86
|
+
payload = JSON.parse(raw || "{}");
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const body = mapCodexHookToHint(payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
91
|
+
if (!body) return;
|
|
92
|
+
const port = await readHooksPort();
|
|
93
|
+
if (port) await post(port, body);
|
|
94
|
+
}
|
|
95
|
+
void main().finally(() => process.exit(0));
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/hook-bridge.ts
|
|
4
|
+
import http from "http";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import path from "path";
|
|
10
|
+
var CONFIG_DIR_NAME = ".cloud-usage";
|
|
11
|
+
var HOOKS_PORT_FILE_NAME = "hooks-port";
|
|
12
|
+
function hooksPortFilePath(home = homedir()) {
|
|
13
|
+
return path.join(home, CONFIG_DIR_NAME, HOOKS_PORT_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
async function readHooksPort(home = homedir()) {
|
|
16
|
+
try {
|
|
17
|
+
const port = Number((await fs.readFile(hooksPortFilePath(home), "utf8")).trim());
|
|
18
|
+
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
19
|
+
} catch {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/hook-bridge-core.ts
|
|
25
|
+
var TOOL_CATEGORY = {
|
|
26
|
+
Read: "read",
|
|
27
|
+
Glob: "search",
|
|
28
|
+
Grep: "search",
|
|
29
|
+
Bash: "run",
|
|
30
|
+
Edit: "write",
|
|
31
|
+
Write: "write",
|
|
32
|
+
MultiEdit: "write",
|
|
33
|
+
NotebookEdit: "write",
|
|
34
|
+
WebFetch: "web",
|
|
35
|
+
WebSearch: "web",
|
|
36
|
+
Task: "task"
|
|
37
|
+
};
|
|
38
|
+
function toolCategoryOf(toolName) {
|
|
39
|
+
return typeof toolName === "string" ? TOOL_CATEGORY[toolName] : void 0;
|
|
40
|
+
}
|
|
41
|
+
function classifyRisk(command) {
|
|
42
|
+
if (typeof command !== "string") return void 0;
|
|
43
|
+
const c = command.toLowerCase();
|
|
44
|
+
if (/\bgit\s+push\b/.test(c) && /(--force|-f)\b/.test(c)) return "force-push";
|
|
45
|
+
if (/\bgit\s+(reset\s+--hard|rebase\b|filter-branch|commit\s+--amend)/.test(c)) return "history-rewrite";
|
|
46
|
+
if (/\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r|\brm\s+-r\b[^|;&]*-f|\brm\s+-f\b[^|;&]*-r/.test(c)) return "recursive-delete";
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
function asRecord(value) {
|
|
50
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
51
|
+
}
|
|
52
|
+
function mapHookToHint(event, payload, at) {
|
|
53
|
+
const p = asRecord(payload);
|
|
54
|
+
const sessionId = typeof p.session_id === "string" ? p.session_id : void 0;
|
|
55
|
+
if (!sessionId) return null;
|
|
56
|
+
switch (event) {
|
|
57
|
+
case "Notification": {
|
|
58
|
+
const type = typeof p.notification_type === "string" ? p.notification_type.toLowerCase() : "";
|
|
59
|
+
const kind = type.includes("permission") ? "permission" : "input_required";
|
|
60
|
+
return { sessionId, kind, at };
|
|
61
|
+
}
|
|
62
|
+
case "PreToolUse": {
|
|
63
|
+
const command = asRecord(p.tool_input).command;
|
|
64
|
+
const risk = p.tool_name === "Bash" ? classifyRisk(command) : void 0;
|
|
65
|
+
if (risk) return { sessionId, kind: "risk", at, riskCategory: risk };
|
|
66
|
+
const toolCategory = toolCategoryOf(p.tool_name);
|
|
67
|
+
return { sessionId, kind: "activity", at, ...toolCategory ? { toolCategory } : {} };
|
|
68
|
+
}
|
|
69
|
+
case "PostToolUse": {
|
|
70
|
+
const toolCategory = toolCategoryOf(p.tool_name);
|
|
71
|
+
const durationMs = typeof p.duration_ms === "number" && Number.isFinite(p.duration_ms) ? p.duration_ms : void 0;
|
|
72
|
+
return {
|
|
73
|
+
sessionId,
|
|
74
|
+
kind: "activity",
|
|
75
|
+
at,
|
|
76
|
+
...toolCategory ? { toolCategory } : {},
|
|
77
|
+
...durationMs !== void 0 ? { durationMs } : {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "PreCompact":
|
|
81
|
+
return { sessionId, kind: "context_saturated", at };
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/hook-bridge.ts
|
|
88
|
+
var MAX_STDIN_BYTES = 64 * 1024;
|
|
89
|
+
var POST_TIMEOUT_MS = 200;
|
|
90
|
+
function readStdin() {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
let data = "";
|
|
93
|
+
let size = 0;
|
|
94
|
+
process.stdin.setEncoding("utf8");
|
|
95
|
+
process.stdin.on("data", (chunk) => {
|
|
96
|
+
size += Buffer.byteLength(chunk);
|
|
97
|
+
if (size <= MAX_STDIN_BYTES) data += chunk;
|
|
98
|
+
});
|
|
99
|
+
process.stdin.on("end", () => resolve(data));
|
|
100
|
+
process.stdin.on("error", () => resolve(data));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function post(port, body) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const data = JSON.stringify(body);
|
|
106
|
+
const request = http.request(
|
|
107
|
+
{
|
|
108
|
+
host: "127.0.0.1",
|
|
109
|
+
port,
|
|
110
|
+
path: "/hooks/claude",
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(data) },
|
|
113
|
+
timeout: POST_TIMEOUT_MS
|
|
114
|
+
},
|
|
115
|
+
() => resolve()
|
|
116
|
+
);
|
|
117
|
+
request.on("error", () => resolve());
|
|
118
|
+
request.on("timeout", () => {
|
|
119
|
+
request.destroy();
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
request.end(data);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function main() {
|
|
126
|
+
const event = process.argv[2] ?? "";
|
|
127
|
+
const raw = await readStdin();
|
|
128
|
+
let payload;
|
|
129
|
+
try {
|
|
130
|
+
payload = JSON.parse(raw || "{}");
|
|
131
|
+
} catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const body = mapHookToHint(event, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
135
|
+
if (!body) return;
|
|
136
|
+
const port = await readHooksPort();
|
|
137
|
+
if (port) await post(port, body);
|
|
138
|
+
}
|
|
139
|
+
void main().finally(() => process.exit(0));
|