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 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));