m0squared-indicator 1.0.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,23 @@
1
+ Copyright (c) 2026 morius (M-zero-Squared)
2
+
3
+ All rights reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the
6
+ exclusive property of morius. The Software is provided for personal,
7
+ non-commercial use only.
8
+
9
+ You are permitted to:
10
+ - Install and use the Software on your own devices for personal use
11
+ - Share the installation command so others may install the Software
12
+
13
+ You are NOT permitted to:
14
+ - Copy, modify, or distribute the source code
15
+ - Use the Software for commercial purposes without written permission
16
+ - Publish derivative works based on this Software
17
+ - Remove or alter this copyright notice
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
21
+ OTHER LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.
22
+
23
+ Contact: github.com/m0squared/m0squared
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ <div align="center">
2
+
3
+ <pre>
4
+ ███╗ ███╗ ██████╗<sup>2</sup>
5
+ ████╗ ████║██╔═══██╗
6
+ ██╔████╔██║██║ ██║
7
+ ██║╚██╔╝██║╚██████╔╝
8
+ ╚═╝ ╚═╝ ╚═════╝
9
+ </pre>
10
+
11
+ ### **M0²**
12
+
13
+ *Real-time token usage indicator for AI coding agents*
14
+
15
+ ---
16
+
17
+ [![npm](https://img.shields.io/npm/v/m0squared-indicator?color=cyan&label=npm)](https://www.npmjs.com/package/m0squared-indicator)
18
+ [![npm downloads](https://img.shields.io/npm/dm/m0squared-indicator?color=cyan)](https://www.npmjs.com/package/m0squared-indicator)
19
+ [![license](https://img.shields.io/badge/license-Personal%20Use-yellow)](#license)
20
+ [![made with love](https://img.shields.io/badge/made%20with-%E2%9D%A4-red)](https://github.com/m0squared/m0s-indicator)
21
+ [![built by a Claude Code lover](https://img.shields.io/badge/built%20by-Claude%20Code%20lover-blueviolet)](https://claude.ai/code)
22
+
23
+ </div>
24
+
25
+ ---
26
+
27
+ ## What is M0²?
28
+
29
+ **M0²** is a live HUD (Heads-Up Display) that sits inside your AI coding agent and shows you exactly how much of your token quota you've consumed — in real time, right where you work.
30
+
31
+ No more getting cut off mid-task. No more opening dashboards. No more guessing.
32
+
33
+ > *Start full. Watch it drain. Know when to stop.*
34
+
35
+ ```
36
+ [Pro] 🔋 [████████░░░░░░░░░░░░] 42% ↺ 3h 20m 📝 [████░░░░░░░░░░░] 18%
37
+ [Max] 🔋 [████████░░░░░░░░░░░░] 42% ↺ 3h 20m 📝 [████░░░░░░░░░░░] 18%
38
+ [PAYG] 💸 $0.0241 📝 [███████░░░░░░░░] 33%
39
+ [Free] 📝 [█░░░░░░░░░░░░░░░░░░░] 5%
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Supported Agents
45
+
46
+ | Agent | Status | Integration |
47
+ |---|---|---|
48
+ | **Claude Code** | ✅ Full support | Native `statusLine` hook — live in the status bar |
49
+ | **Codex CLI** | ✅ Supported | `PostToolUse` hook — tracks every response |
50
+ | **Gemini CLI** | ✅ Supported | `AfterTool` hook — tracks every response |
51
+
52
+ > More agents coming. PRs welcome.
53
+
54
+ ---
55
+
56
+ ## Install
57
+
58
+ Pick your method — they all do the same thing.
59
+
60
+ ### npx *(recommended — no install needed)*
61
+ ```bash
62
+ npx m0squared-indicator install
63
+ ```
64
+
65
+ ### npm
66
+ ```bash
67
+ npm install -g m0squared-indicator
68
+ m0squared-indicator install
69
+ ```
70
+
71
+ ### pip
72
+ ```bash
73
+ pip install m0squared-indicator
74
+ m0squared-indicator install
75
+ ```
76
+
77
+ ### curl *(Linux / macOS)*
78
+ ```bash
79
+ curl -sSL https://raw.githubusercontent.com/m0squared/m0s-indicator/main/scripts/install.sh | bash
80
+ ```
81
+
82
+ ### PowerShell *(Windows)*
83
+ ```powershell
84
+ irm https://raw.githubusercontent.com/m0squared/m0s-indicator/main/scripts/install.ps1 | iex
85
+ ```
86
+
87
+ ---
88
+
89
+ ## How it works
90
+
91
+ M0² auto-detects which AI agents are installed on your machine and patches their config files silently. After a restart, the HUD appears automatically.
92
+
93
+ ```
94
+ M0² v1.0.0 Universal AI Agent HUD
95
+ by morius
96
+
97
+ Scanning for AI agents…
98
+
99
+ ✓ Claude Code detected
100
+ ✓ Codex CLI detected
101
+ ✗ Gemini CLI not found
102
+
103
+ Installing for: Claude Code, Codex CLI
104
+
105
+ ✓ M0² installed successfully!
106
+ Restart your agent to see the HUD.
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Usage
112
+
113
+ ```bash
114
+ m0squared-indicator install # install for all detected agents
115
+ m0squared-indicator install --agent claude-code # target one agent
116
+ m0squared-indicator uninstall # remove from all agents
117
+ m0squared-indicator update # update to latest version
118
+ m0squared-indicator agents # list detected agents and status
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Configuration
124
+
125
+ After install, a config file is created at `~/.m0squared/config.json`:
126
+
127
+ ```json
128
+ {
129
+ "plan": "auto",
130
+ "bar_width": 20,
131
+ "codex": {
132
+ "session_token_limit": 100000
133
+ },
134
+ "gemini": {
135
+ "session_token_limit": 100000
136
+ }
137
+ }
138
+ ```
139
+
140
+ | Key | Values | Description |
141
+ |---|---|---|
142
+ | `plan` | `auto` `pro` `max` `payg` `free` | Override plan auto-detection |
143
+ | `bar_width` | number | Width of the progress bar (default: 20) |
144
+ | `*.session_token_limit` | number | Token limit for Codex / Gemini sessions |
145
+
146
+ > **Auto-detection:** Claude Code Pro/Max is detected automatically via the `rate_limits` field. Set `"plan": "max"` manually if you're on Max.
147
+
148
+ ---
149
+
150
+ ## Uninstall
151
+
152
+ ```bash
153
+ npx m0squared-indicator uninstall
154
+ ```
155
+
156
+ This removes all hooks from your agent configs and deletes `~/.m0squared/`.
157
+
158
+ ---
159
+
160
+ ## Why M0²?
161
+
162
+ Built out of pure love for [Claude Code](https://claude.ai/code) — and frustration with hitting token limits mid-session without any warning.
163
+
164
+ M0² was born from a simple idea: *your tools should tell you when you're running out of fuel.*
165
+
166
+ > **Built by a Claude Code lover, with Claude Code.**
167
+ > *— haddad med / morius*
168
+
169
+ ---
170
+
171
+ ## License
172
+
173
+ © 2026 morius (M-zero-Squared / haddad med). All rights reserved.
174
+
175
+ Free for personal use. No redistribution or commercial use without written permission.
176
+ See [LICENSE](./LICENSE) for full terms.
177
+
178
+ ---
179
+
180
+ <div align="center">
181
+
182
+ Made with ❤️ by **haddad med** · [github.com/m0squared](https://github.com/m0squared)
183
+
184
+ *If M0² saved your session — give it a ⭐*
185
+
186
+ </div>
package/bin/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // © 2026 morius (M-zero-Squared). All rights reserved.
5
+ // Free for personal use. No redistribution without permission.
6
+ // https://github.com/m0squared/m0squared
7
+
8
+ const args = process.argv.slice(2);
9
+ const ui = require('../src/ui');
10
+
11
+ // Parse --agent flag
12
+ let agentFlag = null;
13
+ const agentIdx = args.indexOf('--agent');
14
+ if (agentIdx !== -1 && args[agentIdx + 1]) {
15
+ agentFlag = args[agentIdx + 1];
16
+ }
17
+
18
+ const command = args.find(a => !a.startsWith('-') && a !== agentFlag);
19
+
20
+ const opts = { agent: agentFlag };
21
+
22
+ switch (command) {
23
+ case 'install':
24
+ case 'i':
25
+ require('../src/commands/install')(opts).catch(e => {
26
+ ui.error(e.message);
27
+ process.exit(1);
28
+ });
29
+ break;
30
+
31
+ case 'uninstall':
32
+ case 'remove':
33
+ case 'u':
34
+ require('../src/commands/uninstall')(opts).catch(e => {
35
+ ui.error(e.message);
36
+ process.exit(1);
37
+ });
38
+ break;
39
+
40
+ case 'update':
41
+ require('../src/commands/update')(opts).catch(e => {
42
+ ui.error(e.message);
43
+ process.exit(1);
44
+ });
45
+ break;
46
+
47
+ case 'agents': {
48
+ const { detectAll } = require('../src/detect');
49
+ const AGENTS = {
50
+ 'claude-code': require('../src/agents/claude-code'),
51
+ 'codex': require('../src/agents/codex'),
52
+ 'gemini': require('../src/agents/gemini'),
53
+ };
54
+ ui.banner();
55
+ ui.section('Agent status:\n');
56
+ const detected = detectAll();
57
+ for (const [name, agent] of Object.entries(AGENTS)) {
58
+ if (!detected[name]) {
59
+ ui.agentLine(name, 'missing');
60
+ } else if (agent.isInstalled()) {
61
+ ui.agentLine(name, 'found', 'M0² installed');
62
+ } else {
63
+ ui.agentLine(name, 'found', 'M0² not installed');
64
+ }
65
+ }
66
+ console.log();
67
+ break;
68
+ }
69
+
70
+ case '--version':
71
+ case '-v':
72
+ case 'version': {
73
+ const pkg = require('../package.json');
74
+ console.log(`m0squared v${pkg.version}`);
75
+ break;
76
+ }
77
+
78
+ case '--help':
79
+ case '-h':
80
+ case 'help':
81
+ default:
82
+ ui.showHelp();
83
+ break;
84
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "plan": "auto",
3
+ "bar_width": 20,
4
+ "codex": {
5
+ "session_token_limit": 100000
6
+ },
7
+ "gemini": {
8
+ "session_token_limit": 100000
9
+ }
10
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ # © 2026 morius (M-zero-Squared). All rights reserved.
3
+ # https://github.com/m0squared/m0squared
4
+ """
5
+ M0² Codex CLI Hook — PostToolUse
6
+ Reads the session transcript, calculates token usage,
7
+ and writes state to ~/.m0squared/codex-state.json
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ M0SQ_DIR = Path.home() / ".m0squared"
16
+ STATE_FILE = M0SQ_DIR / "codex-state.json"
17
+ CONFIG_FILE = M0SQ_DIR / "config.json"
18
+
19
+ def load_config() -> dict:
20
+ if CONFIG_FILE.exists():
21
+ try:
22
+ return json.loads(CONFIG_FILE.read_text())
23
+ except Exception:
24
+ pass
25
+ return {}
26
+
27
+ def sum_tokens_from_transcript(transcript_path: str) -> dict:
28
+ """Sum all token usage from a JSONL transcript."""
29
+ total = {"input_tokens": 0, "output_tokens": 0,
30
+ "cache_read": 0, "cache_create": 0}
31
+ try:
32
+ with open(transcript_path) as f:
33
+ for line in f:
34
+ try:
35
+ entry = json.loads(line.strip())
36
+ usage = (entry.get("message") or {}).get("usage") or \
37
+ entry.get("usage") or {}
38
+ total["input_tokens"] += usage.get("input_tokens", 0)
39
+ total["output_tokens"] += usage.get("output_tokens", 0)
40
+ total["cache_read"] += usage.get("cache_read_input_tokens", 0)
41
+ total["cache_create"] += usage.get("cache_creation_input_tokens", 0)
42
+ except Exception:
43
+ pass
44
+ except Exception:
45
+ pass
46
+ return total
47
+
48
+ def main() -> None:
49
+ try:
50
+ hook_input = json.loads(sys.stdin.read())
51
+ except Exception:
52
+ sys.exit(0)
53
+
54
+ transcript_path = hook_input.get("transcript_path", "")
55
+ config = load_config()
56
+
57
+ token_limit = (config.get("codex") or {}).get("session_token_limit", 100000)
58
+
59
+ tokens = sum_tokens_from_transcript(transcript_path)
60
+ total_used = tokens["input_tokens"] + tokens["output_tokens"]
61
+ pct = min(100.0, (total_used / token_limit) * 100) if token_limit > 0 else 0
62
+
63
+ M0SQ_DIR.mkdir(parents=True, exist_ok=True)
64
+
65
+ state = {
66
+ "_agent": "codex",
67
+ "_updated": datetime.now(timezone.utc).isoformat(),
68
+ "context_window": {
69
+ "used_percentage": round(pct, 1),
70
+ "total_input_tokens": tokens["input_tokens"],
71
+ "total_output_tokens": tokens["output_tokens"],
72
+ },
73
+ "cost": {
74
+ "total_cost_usd": 0
75
+ },
76
+ }
77
+
78
+ STATE_FILE.write_text(json.dumps(state, indent=2))
79
+
80
+ # Allow hook to continue (don't block)
81
+ print(json.dumps({"continue": True}))
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ # © 2026 morius (M-zero-Squared). All rights reserved.
3
+ # https://github.com/m0squared/m0squared
4
+ """
5
+ M0² Gemini CLI Hook — AfterTool
6
+ Reads the session transcript, calculates token usage,
7
+ and writes state to ~/.m0squared/gemini-state.json
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ M0SQ_DIR = Path.home() / ".m0squared"
16
+ STATE_FILE = M0SQ_DIR / "gemini-state.json"
17
+ CONFIG_FILE = M0SQ_DIR / "config.json"
18
+
19
+ def load_config() -> dict:
20
+ if CONFIG_FILE.exists():
21
+ try:
22
+ return json.loads(CONFIG_FILE.read_text())
23
+ except Exception:
24
+ pass
25
+ return {}
26
+
27
+ def sum_tokens_from_transcript(transcript_path: str) -> dict:
28
+ """Sum all token usage from a JSONL transcript."""
29
+ total = {"input_tokens": 0, "output_tokens": 0}
30
+ try:
31
+ with open(transcript_path) as f:
32
+ for line in f:
33
+ try:
34
+ entry = json.loads(line.strip())
35
+ # Gemini CLI transcript format
36
+ usage = entry.get("usageMetadata") or \
37
+ (entry.get("message") or {}).get("usage") or \
38
+ entry.get("usage") or {}
39
+ total["input_tokens"] += (
40
+ usage.get("promptTokenCount") or
41
+ usage.get("input_tokens", 0)
42
+ )
43
+ total["output_tokens"] += (
44
+ usage.get("candidatesTokenCount") or
45
+ usage.get("output_tokens", 0)
46
+ )
47
+ except Exception:
48
+ pass
49
+ except Exception:
50
+ pass
51
+ return total
52
+
53
+ def main() -> None:
54
+ try:
55
+ hook_input = json.loads(sys.stdin.read())
56
+ except Exception:
57
+ sys.exit(0)
58
+
59
+ transcript_path = hook_input.get("transcript_path", "")
60
+ config = load_config()
61
+
62
+ token_limit = (config.get("gemini") or {}).get("session_token_limit", 100000)
63
+
64
+ tokens = sum_tokens_from_transcript(transcript_path)
65
+ total_used = tokens["input_tokens"] + tokens["output_tokens"]
66
+ pct = min(100.0, (total_used / token_limit) * 100) if token_limit > 0 else 0
67
+
68
+ M0SQ_DIR.mkdir(parents=True, exist_ok=True)
69
+
70
+ state = {
71
+ "_agent": "gemini",
72
+ "_updated": datetime.now(timezone.utc).isoformat(),
73
+ "context_window": {
74
+ "used_percentage": round(pct, 1),
75
+ "total_input_tokens": tokens["input_tokens"],
76
+ "total_output_tokens": tokens["output_tokens"],
77
+ },
78
+ "cost": {
79
+ "total_cost_usd": 0
80
+ },
81
+ }
82
+
83
+ STATE_FILE.write_text(json.dumps(state, indent=2))
84
+
85
+ # Gemini hooks expect exit 0 for success
86
+ sys.exit(0)
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ # © 2026 morius (M-zero-Squared). All rights reserved.
3
+ # https://github.com/m0squared/m0squared
4
+ """
5
+ M0² — Universal AI Agent HUD
6
+ Status line script: receives JSON from stdin (Claude Code statusLine mode)
7
+ or reads state file (Codex / Gemini mode).
8
+ """
9
+
10
+ import json
11
+ import sys
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ M0SQ_DIR = Path.home() / ".m0squared"
16
+ CONFIG_FILE = M0SQ_DIR / "config.json"
17
+
18
+ # ── ANSI ──────────────────────────────────────────────────────────────────────
19
+ GREEN = "\033[92m"
20
+ YELLOW = "\033[93m"
21
+ RED = "\033[91m"
22
+ CYAN = "\033[96m"
23
+ BOLD = "\033[1m"
24
+ DIM = "\033[2m"
25
+ RESET = "\033[0m"
26
+
27
+ # ── Config ────────────────────────────────────────────────────────────────────
28
+
29
+ def load_config() -> dict:
30
+ if CONFIG_FILE.exists():
31
+ try:
32
+ return json.loads(CONFIG_FILE.read_text())
33
+ except Exception:
34
+ pass
35
+ return {}
36
+
37
+ # ── Rendering helpers ─────────────────────────────────────────────────────────
38
+
39
+ def bar(pct: float | None, width: int) -> str:
40
+ if pct is None:
41
+ return "[" + "?" * width + "]"
42
+ filled = min(width, max(0, round(width * pct / 100)))
43
+ return "[" + "█" * filled + "░" * (width - filled) + "]"
44
+
45
+ def color(pct: float | None) -> str:
46
+ if pct is None: return DIM
47
+ if pct >= 80: return RED
48
+ if pct >= 50: return YELLOW
49
+ return GREEN
50
+
51
+ def fmt_pct(pct: float | None) -> str:
52
+ return f"{pct:.0f}%" if pct is not None else "?%"
53
+
54
+ def time_until(ts: float) -> str:
55
+ remaining = ts - datetime.now(timezone.utc).timestamp()
56
+ if remaining <= 0: return "resetting…"
57
+ h = int(remaining // 3600)
58
+ m = int((remaining % 3600) // 60)
59
+ return f"{h}h {m}m" if h else f"{m}m"
60
+
61
+ BADGES = {
62
+ "pro": (CYAN, "Pro"),
63
+ "max": (BOLD, "Max"),
64
+ "payg": (YELLOW, "PAYG"),
65
+ "free": (DIM, "Free"),
66
+ }
67
+
68
+ def badge(plan: str) -> str:
69
+ c, label = BADGES.get(plan, (DIM, plan.upper()))
70
+ return f"{BOLD}{c}[{label}]{RESET}"
71
+
72
+ AGENT_BADGES = {
73
+ "claude-code": f"{CYAN}Claude{RESET}",
74
+ "codex": f"\033[35mCodex{RESET}",
75
+ "gemini": f"\033[34mGemini{RESET}",
76
+ }
77
+
78
+ def detect_plan(data: dict, config: dict) -> str:
79
+ user_plan = config.get("plan", "auto").lower()
80
+ if user_plan != "auto":
81
+ return user_plan
82
+ if data.get("rate_limits"):
83
+ return "pro"
84
+ cost = (data.get("cost") or {}).get("total_cost_usd", 0)
85
+ if cost and float(cost) > 0:
86
+ return "payg"
87
+ return "free"
88
+
89
+ # ── Render ────────────────────────────────────────────────────────────────────
90
+
91
+ def render(data: dict, config: dict) -> None:
92
+ plan = detect_plan(data, config)
93
+ width = int(config.get("bar_width", 20))
94
+ ctx = data.get("context_window") or {}
95
+ rate = data.get("rate_limits")
96
+ cost = data.get("cost") or {}
97
+ agent = data.get("_agent", "claude-code")
98
+
99
+ parts: list[str] = []
100
+
101
+ # Agent badge (for multi-agent state files)
102
+ if agent != "claude-code":
103
+ parts.append(AGENT_BADGES.get(agent, agent))
104
+
105
+ parts.append(badge(plan))
106
+
107
+ # Subscription rate limit bar (Pro / Max)
108
+ if plan in ("pro", "max") and rate:
109
+ five = rate.get("five_hour", {})
110
+ pct = five.get("used_percentage")
111
+ resets = five.get("resets_at")
112
+ c = color(pct)
113
+ b = bar(pct, width)
114
+ p = fmt_pct(pct)
115
+ rt = f" {DIM}↺ {time_until(resets)}{RESET}" if resets else ""
116
+ parts.append(f"🔋 {c}{b} {p}{RESET}{rt}")
117
+
118
+ # PAYG cost
119
+ elif plan == "payg":
120
+ usd = float(cost.get("total_cost_usd", 0) or 0)
121
+ parts.append(f"💸 {YELLOW}${usd:.4f}{RESET}")
122
+
123
+ # Context window bar
124
+ ctx_pct = ctx.get("used_percentage")
125
+ if ctx_pct is not None:
126
+ c = color(ctx_pct)
127
+ b = bar(ctx_pct, max(10, int(width * 0.75)))
128
+ p = fmt_pct(ctx_pct)
129
+ parts.append(f"📝 {c}{b} {p}{RESET}")
130
+
131
+ if parts:
132
+ print(" ".join(parts))
133
+
134
+ # ── Main ──────────────────────────────────────────────────────────────────────
135
+
136
+ def main() -> None:
137
+ config = load_config()
138
+
139
+ # Try stdin first (Claude Code statusLine mode)
140
+ if not sys.stdin.isatty():
141
+ raw = sys.stdin.read()
142
+ if raw.strip():
143
+ try:
144
+ data = json.loads(raw)
145
+ render(data, config)
146
+ return
147
+ except Exception:
148
+ pass
149
+
150
+ # Fall back to state files (Codex / Gemini mode)
151
+ for agent in ("codex", "gemini"):
152
+ state_file = M0SQ_DIR / f"{agent}-state.json"
153
+ if state_file.exists():
154
+ try:
155
+ data = json.loads(state_file.read_text())
156
+ data.setdefault("_agent", agent)
157
+ render(data, config)
158
+ return
159
+ except Exception:
160
+ pass
161
+
162
+ if __name__ == "__main__":
163
+ main()
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "m0squared-indicator",
3
+ "version": "1.0.0",
4
+ "description": "M0² — Universal AI Agent Token Usage HUD for Claude Code, Codex CLI & Gemini CLI",
5
+ "keywords": ["claude-code", "codex", "gemini", "ai", "token", "usage", "hud", "indicator", "m0squared"],
6
+ "author": {
7
+ "name": "haddad med",
8
+ "url": "https://github.com/m0squared"
9
+ },
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/m0squared/m0s-indicator.git"
14
+ },
15
+ "homepage": "https://github.com/m0squared/m0s-indicator",
16
+ "bugs": {
17
+ "url": "https://github.com/m0squared/m0s-indicator/issues"
18
+ },
19
+ "bin": {
20
+ "m0squared-indicator": "./bin/cli.js"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "src",
25
+ "core",
26
+ "config.default.json",
27
+ "LICENSE"
28
+ ],
29
+ "engines": {
30
+ "node": ">=16"
31
+ },
32
+ "scripts": {
33
+ "test": "node bin/cli.js --help"
34
+ }
35
+ }
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const HOME = os.homedir();
8
+ const CLAUDE_DIR = path.join(HOME, '.claude');
9
+ const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
10
+
11
+ function isInstalled() {
12
+ try {
13
+ if (!fs.existsSync(SETTINGS_PATH)) return false;
14
+ const s = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
15
+ return !!(s.statusLine && s.statusLine.command && s.statusLine.command.includes('m0squared'));
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ function install(installDir) {
22
+ const scriptPath = path.join(installDir, 'indicator.py');
23
+
24
+ // Ensure ~/.claude exists
25
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
26
+
27
+ // Read or init settings.json
28
+ let settings = {};
29
+ if (fs.existsSync(SETTINGS_PATH)) {
30
+ try {
31
+ settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
32
+ } catch {
33
+ settings = {};
34
+ }
35
+ }
36
+
37
+ settings.statusLine = {
38
+ type: 'command',
39
+ command: `python3 ${scriptPath}`,
40
+ };
41
+
42
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
43
+ return { ok: true };
44
+ }
45
+
46
+ function uninstall() {
47
+ if (!fs.existsSync(SETTINGS_PATH)) return { ok: true };
48
+
49
+ try {
50
+ const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
51
+ if (settings.statusLine && settings.statusLine.command &&
52
+ settings.statusLine.command.includes('m0squared')) {
53
+ delete settings.statusLine;
54
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
55
+ }
56
+ return { ok: true };
57
+ } catch (e) {
58
+ return { ok: false, error: e.message };
59
+ }
60
+ }
61
+
62
+ module.exports = { isInstalled, install, uninstall };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const HOME = os.homedir();
8
+ const CODEX_DIR = path.join(HOME, '.codex');
9
+ const CONFIG_PATH = path.join(CODEX_DIR, 'config.toml');
10
+ const HOOKS_PATH = path.join(CODEX_DIR, 'hooks.json');
11
+
12
+ function isInstalled() {
13
+ try {
14
+ if (!fs.existsSync(HOOKS_PATH)) return false;
15
+ const h = JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
16
+ const postHooks = h.PostToolUse || [];
17
+ return postHooks.some(h => JSON.stringify(h).includes('m0squared'));
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ function install(installDir) {
24
+ const hookScript = path.join(installDir, 'hooks', 'codex-hook.py');
25
+
26
+ fs.mkdirSync(CODEX_DIR, { recursive: true });
27
+
28
+ // Enable hooks in config.toml
29
+ let toml = '';
30
+ if (fs.existsSync(CONFIG_PATH)) {
31
+ toml = fs.readFileSync(CONFIG_PATH, 'utf8');
32
+ }
33
+
34
+ if (!toml.includes('[features]')) {
35
+ toml += '\n[features]\ncodex_hooks = true\n';
36
+ } else if (!toml.includes('codex_hooks')) {
37
+ toml = toml.replace('[features]', '[features]\ncodex_hooks = true');
38
+ }
39
+
40
+ fs.writeFileSync(CONFIG_PATH, toml);
41
+
42
+ // Add PostToolUse hook to hooks.json
43
+ let hooks = {};
44
+ if (fs.existsSync(HOOKS_PATH)) {
45
+ try {
46
+ hooks = JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
47
+ } catch {
48
+ hooks = {};
49
+ }
50
+ }
51
+
52
+ if (!hooks.PostToolUse) hooks.PostToolUse = {};
53
+ if (!hooks.PostToolUse['*']) hooks.PostToolUse['*'] = [];
54
+
55
+ // Remove any existing m0squared hook
56
+ hooks.PostToolUse['*'] = hooks.PostToolUse['*'].filter(
57
+ h => !JSON.stringify(h).includes('m0squared')
58
+ );
59
+
60
+ hooks.PostToolUse['*'].push({
61
+ command: `python3 ${hookScript}`,
62
+ timeout: 5,
63
+ statusMessage: 'M0² tracking tokens…',
64
+ });
65
+
66
+ fs.writeFileSync(HOOKS_PATH, JSON.stringify(hooks, null, 2) + '\n');
67
+ return { ok: true };
68
+ }
69
+
70
+ function uninstall() {
71
+ if (!fs.existsSync(HOOKS_PATH)) return { ok: true };
72
+
73
+ try {
74
+ const hooks = JSON.parse(fs.readFileSync(HOOKS_PATH, 'utf8'));
75
+ if (hooks.PostToolUse && hooks.PostToolUse['*']) {
76
+ hooks.PostToolUse['*'] = hooks.PostToolUse['*'].filter(
77
+ h => !JSON.stringify(h).includes('m0squared')
78
+ );
79
+ }
80
+ fs.writeFileSync(HOOKS_PATH, JSON.stringify(hooks, null, 2) + '\n');
81
+ return { ok: true };
82
+ } catch (e) {
83
+ return { ok: false, error: e.message };
84
+ }
85
+ }
86
+
87
+ module.exports = { isInstalled, install, uninstall };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const HOME = os.homedir();
8
+ const GEMINI_DIR = path.join(HOME, '.gemini');
9
+ const SETTINGS_PATH = path.join(GEMINI_DIR, 'settings.json');
10
+
11
+ function isInstalled() {
12
+ try {
13
+ if (!fs.existsSync(SETTINGS_PATH)) return false;
14
+ const s = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
15
+ const afterTool = s.hooks && s.hooks.AfterTool;
16
+ return !!(afterTool && JSON.stringify(afterTool).includes('m0squared'));
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function install(installDir) {
23
+ const hookScript = path.join(installDir, 'hooks', 'gemini-hook.py');
24
+
25
+ fs.mkdirSync(GEMINI_DIR, { recursive: true });
26
+
27
+ // Read or init settings.json
28
+ let settings = {};
29
+ if (fs.existsSync(SETTINGS_PATH)) {
30
+ try {
31
+ settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
32
+ } catch {
33
+ settings = {};
34
+ }
35
+ }
36
+
37
+ if (!settings.hooks) settings.hooks = {};
38
+
39
+ if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
40
+
41
+ // Remove any existing m0squared hook
42
+ settings.hooks.AfterTool = settings.hooks.AfterTool.filter(
43
+ entry => !JSON.stringify(entry).includes('m0squared')
44
+ );
45
+
46
+ settings.hooks.AfterTool.push({
47
+ matcher: '.*',
48
+ hooks: [
49
+ {
50
+ name: 'm0squared-tracker',
51
+ type: 'command',
52
+ command: `python3 ${hookScript}`,
53
+ timeout: 5000,
54
+ },
55
+ ],
56
+ });
57
+
58
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
59
+ return { ok: true };
60
+ }
61
+
62
+ function uninstall() {
63
+ if (!fs.existsSync(SETTINGS_PATH)) return { ok: true };
64
+
65
+ try {
66
+ const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
67
+ if (settings.hooks && settings.hooks.AfterTool) {
68
+ settings.hooks.AfterTool = settings.hooks.AfterTool.filter(
69
+ entry => !JSON.stringify(entry).includes('m0squared')
70
+ );
71
+ }
72
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
73
+ return { ok: true };
74
+ } catch (e) {
75
+ return { ok: false, error: e.message };
76
+ }
77
+ }
78
+
79
+ module.exports = { isInstalled, install, uninstall };
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const ui = require('../ui');
8
+ const { detectAll, getInstallDir } = require('../detect');
9
+
10
+ const AGENTS = {
11
+ 'claude-code': require('../agents/claude-code'),
12
+ 'codex': require('../agents/codex'),
13
+ 'gemini': require('../agents/gemini'),
14
+ };
15
+
16
+ const AGENT_LABELS = {
17
+ 'claude-code': 'Claude Code',
18
+ 'codex': 'Codex CLI',
19
+ 'gemini': 'Gemini CLI',
20
+ };
21
+
22
+ module.exports = async function install(opts = {}) {
23
+ ui.banner();
24
+
25
+ const installDir = getInstallDir();
26
+ const targetAgent = opts.agent || null;
27
+
28
+ // Detect agents
29
+ ui.section('Scanning for AI agents…\n');
30
+ const detected = detectAll();
31
+
32
+ const toInstall = [];
33
+
34
+ for (const [name, agent] of Object.entries(AGENTS)) {
35
+ if (targetAgent && name !== targetAgent) continue;
36
+
37
+ if (!detected[name]) {
38
+ ui.agentLine(name, 'missing');
39
+ } else if (agent.isInstalled()) {
40
+ ui.agentLine(name, 'skip');
41
+ } else {
42
+ ui.agentLine(name, 'found');
43
+ toInstall.push(name);
44
+ }
45
+ }
46
+
47
+ if (toInstall.length === 0) {
48
+ console.log();
49
+ ui.info('Nothing to install — all detected agents already have M0².');
50
+ ui.tip('Run "npx m0squared update" to update to the latest version.');
51
+ console.log();
52
+ return;
53
+ }
54
+
55
+ // Copy core files
56
+ console.log();
57
+ ui.section('Installing core files…');
58
+
59
+ fs.mkdirSync(path.join(installDir, 'hooks'), { recursive: true });
60
+
61
+ // Copy indicator.py
62
+ const indicatorSrc = path.join(__dirname, '../../core/indicator.py');
63
+ const indicatorDst = path.join(installDir, 'indicator.py');
64
+ fs.copyFileSync(indicatorSrc, indicatorDst);
65
+ fs.chmodSync(indicatorDst, '755');
66
+
67
+ // Copy hook scripts
68
+ for (const hookFile of ['codex-hook.py', 'gemini-hook.py']) {
69
+ const src = path.join(__dirname, '../../core/hooks', hookFile);
70
+ const dst = path.join(installDir, 'hooks', hookFile);
71
+ if (fs.existsSync(src)) {
72
+ fs.copyFileSync(src, dst);
73
+ fs.chmodSync(dst, '755');
74
+ }
75
+ }
76
+
77
+ // Write default config if not exists
78
+ const configDst = path.join(installDir, 'config.json');
79
+ if (!fs.existsSync(configDst)) {
80
+ const configSrc = path.join(__dirname, '../../config.default.json');
81
+ fs.copyFileSync(configSrc, configDst);
82
+ }
83
+
84
+ // Install per agent
85
+ console.log();
86
+ ui.section(`Installing for: ${toInstall.map(n => AGENT_LABELS[n]).join(', ')}\n`);
87
+
88
+ let anyFailed = false;
89
+
90
+ for (const name of toInstall) {
91
+ ui.agentLine(name, 'installing');
92
+ try {
93
+ const result = AGENTS[name].install(installDir);
94
+ if (result.ok) {
95
+ ui.agentLine(name, 'done');
96
+ } else {
97
+ ui.agentLine(name, 'error', result.error || 'unknown error');
98
+ anyFailed = true;
99
+ }
100
+ } catch (e) {
101
+ ui.agentLine(name, 'error', e.message);
102
+ anyFailed = true;
103
+ }
104
+ }
105
+
106
+ console.log();
107
+
108
+ if (!anyFailed) {
109
+ ui.success('M0² installed successfully!');
110
+ ui.tip('Restart your AI agent to see the HUD.');
111
+ ui.tip(`Config file: ${path.join(installDir, 'config.json')}`);
112
+ console.log();
113
+ } else {
114
+ ui.error('Some agents failed to install. Check the errors above.');
115
+ }
116
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const ui = require('../ui');
8
+ const { getInstallDir } = require('../detect');
9
+
10
+ const AGENTS = {
11
+ 'claude-code': require('../agents/claude-code'),
12
+ 'codex': require('../agents/codex'),
13
+ 'gemini': require('../agents/gemini'),
14
+ };
15
+
16
+ module.exports = async function uninstall(opts = {}) {
17
+ ui.banner();
18
+
19
+ const targetAgent = opts.agent || null;
20
+
21
+ ui.section('Removing M0² from agents…\n');
22
+
23
+ for (const [name, agent] of Object.entries(AGENTS)) {
24
+ if (targetAgent && name !== targetAgent) continue;
25
+
26
+ ui.agentLine(name, 'installing');
27
+ try {
28
+ const result = agent.uninstall();
29
+ if (result.ok) {
30
+ ui.agentLine(name, 'done');
31
+ } else {
32
+ ui.agentLine(name, 'error', result.error || 'unknown');
33
+ }
34
+ } catch (e) {
35
+ ui.agentLine(name, 'error', e.message);
36
+ }
37
+ }
38
+
39
+ // Remove install directory only if uninstalling all
40
+ if (!targetAgent) {
41
+ const installDir = getInstallDir();
42
+ if (fs.existsSync(installDir)) {
43
+ fs.rmSync(installDir, { recursive: true, force: true });
44
+ }
45
+ }
46
+
47
+ console.log();
48
+ ui.success('M0² uninstalled.');
49
+ console.log();
50
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const ui = require('../ui');
8
+ const { getInstallDir } = require('../detect');
9
+
10
+ module.exports = async function update() {
11
+ ui.banner();
12
+ ui.section('Updating M0²…\n');
13
+
14
+ const installDir = getInstallDir();
15
+
16
+ if (!fs.existsSync(installDir)) {
17
+ ui.error('M0² is not installed. Run "npx m0squared install" first.');
18
+ return;
19
+ }
20
+
21
+ // Copy fresh indicator.py
22
+ try {
23
+ const src = path.join(__dirname, '../../core/indicator.py');
24
+ const dst = path.join(installDir, 'indicator.py');
25
+ fs.copyFileSync(src, dst);
26
+ fs.chmodSync(dst, '755');
27
+
28
+ // Copy hook scripts
29
+ for (const hookFile of ['codex-hook.py', 'gemini-hook.py']) {
30
+ const hookSrc = path.join(__dirname, '../../core/hooks', hookFile);
31
+ const hookDst = path.join(installDir, 'hooks', hookFile);
32
+ if (fs.existsSync(hookSrc)) {
33
+ fs.mkdirSync(path.dirname(hookDst), { recursive: true });
34
+ fs.copyFileSync(hookSrc, hookDst);
35
+ fs.chmodSync(hookDst, '755');
36
+ }
37
+ }
38
+
39
+ ui.success('M0² updated to the latest version.');
40
+ ui.tip('Restart your AI agents to apply the update.');
41
+ console.log();
42
+ } catch (e) {
43
+ ui.error(`Update failed: ${e.message}`);
44
+ }
45
+ };
package/src/detect.js ADDED
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ const HOME = os.homedir();
9
+
10
+ function commandExists(cmd) {
11
+ try {
12
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function detectClaudeCode() {
20
+ const hasDir = fs.existsSync(path.join(HOME, '.claude'));
21
+ const hasCmd = commandExists('claude');
22
+ return hasDir || hasCmd;
23
+ }
24
+
25
+ function detectCodex() {
26
+ const hasDir = fs.existsSync(path.join(HOME, '.codex'));
27
+ const hasCmd = commandExists('codex');
28
+ return hasDir || hasCmd;
29
+ }
30
+
31
+ function detectGemini() {
32
+ const hasDir = fs.existsSync(path.join(HOME, '.gemini'));
33
+ const hasCmd = commandExists('gemini');
34
+ return hasDir || hasCmd;
35
+ }
36
+
37
+ function detectAll() {
38
+ return {
39
+ 'claude-code': detectClaudeCode(),
40
+ 'codex': detectCodex(),
41
+ 'gemini': detectGemini(),
42
+ };
43
+ }
44
+
45
+ function getInstallDir() {
46
+ return path.join(HOME, '.m0squared');
47
+ }
48
+
49
+ module.exports = { detectAll, detectClaudeCode, detectCodex, detectGemini, getInstallDir };
package/src/ui.js ADDED
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const R = '\x1b[0m';
4
+ const BOLD = '\x1b[1m';
5
+ const DIM = '\x1b[2m';
6
+ const GREEN = '\x1b[32m';
7
+ const CYAN = '\x1b[36m';
8
+ const YELLOW = '\x1b[33m';
9
+ const RED = '\x1b[31m';
10
+ const WHITE = '\x1b[37m';
11
+
12
+ const AGENT_COLORS = {
13
+ 'claude-code': CYAN,
14
+ 'codex': '\x1b[35m', // magenta
15
+ 'gemini': '\x1b[34m', // blue
16
+ };
17
+
18
+ function banner() {
19
+ console.log(`
20
+ ${BOLD}${WHITE} ███╗ ███╗ ██████╗²${R}
21
+ ${BOLD}${WHITE} ████╗ ████║██╔═══██╗${R}
22
+ ${BOLD}${WHITE} ██╔████╔██║██║ ██║${R} ${DIM}Universal AI Agent HUD${R}
23
+ ${BOLD}${WHITE} ██║╚██╔╝██║╚██████╔╝${R} ${DIM}v1.0.0 by morius${R}
24
+ ${BOLD}${WHITE} ╚═╝ ╚═╝ ╚═════╝${R}
25
+ `);
26
+ }
27
+
28
+ function section(text) {
29
+ console.log(` ${DIM}${text}${R}`);
30
+ }
31
+
32
+ function agentLine(name, status, detail = '') {
33
+ const c = AGENT_COLORS[name] || WHITE;
34
+ const label = name.padEnd(14);
35
+ if (status === 'found') {
36
+ console.log(` ${GREEN}✓${R} ${BOLD}${c}${label}${R} ${DIM}detected${R} ${detail}`);
37
+ } else if (status === 'missing') {
38
+ console.log(` ${DIM}✗ ${label} not found${R}`);
39
+ } else if (status === 'installing') {
40
+ process.stdout.write(` ${YELLOW}→${R} ${BOLD}${c}${label}${R} `);
41
+ } else if (status === 'done') {
42
+ console.log(`${GREEN}done${R}`);
43
+ } else if (status === 'error') {
44
+ console.log(`${RED}failed — ${detail}${R}`);
45
+ } else if (status === 'skip') {
46
+ console.log(` ${DIM}⊘ ${label} already installed${R}`);
47
+ }
48
+ }
49
+
50
+ function success(text) {
51
+ console.log(`\n ${GREEN}${BOLD}✓${R} ${text}\n`);
52
+ }
53
+
54
+ function error(text) {
55
+ console.log(`\n ${RED}${BOLD}✗${R} ${text}\n`);
56
+ }
57
+
58
+ function info(text) {
59
+ console.log(` ${CYAN}i${R} ${text}`);
60
+ }
61
+
62
+ function tip(text) {
63
+ console.log(` ${DIM}${text}${R}`);
64
+ }
65
+
66
+ function showHelp() {
67
+ banner();
68
+ console.log(` ${BOLD}Usage:${R} npx m0squared <command>\n`);
69
+ console.log(` ${BOLD}Commands:${R}`);
70
+ console.log(` ${CYAN}install${R} Install M0² for all detected AI agents`);
71
+ console.log(` ${CYAN}uninstall${R} Remove M0² from all agents`);
72
+ console.log(` ${CYAN}update${R} Update to the latest version`);
73
+ console.log(` ${CYAN}status${R} Show current token usage in terminal`);
74
+ console.log(` ${CYAN}agents${R} List supported agents and their status`);
75
+ console.log(`\n ${BOLD}Options:${R}`);
76
+ console.log(` ${CYAN}--agent${R} Target a specific agent (claude-code, codex, gemini)`);
77
+ console.log(`\n ${DIM}Examples:${R}`);
78
+ console.log(` npx m0squared install`);
79
+ console.log(` npx m0squared install --agent claude-code`);
80
+ console.log(` npx m0squared status\n`);
81
+ }
82
+
83
+ module.exports = { banner, section, agentLine, success, error, info, tip, showHelp, BOLD, DIM, GREEN, CYAN, YELLOW, RED, R };