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 +23 -0
- package/README.md +186 -0
- package/bin/cli.js +84 -0
- package/config.default.json +10 -0
- package/core/hooks/codex-hook.py +84 -0
- package/core/hooks/gemini-hook.py +89 -0
- package/core/indicator.py +163 -0
- package/package.json +35 -0
- package/src/agents/claude-code.js +62 -0
- package/src/agents/codex.js +87 -0
- package/src/agents/gemini.js +79 -0
- package/src/commands/install.js +116 -0
- package/src/commands/uninstall.js +50 -0
- package/src/commands/update.js +45 -0
- package/src/detect.js +49 -0
- package/src/ui.js +83 -0
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
|
+
[](https://www.npmjs.com/package/m0squared-indicator)
|
|
18
|
+
[](https://www.npmjs.com/package/m0squared-indicator)
|
|
19
|
+
[](#license)
|
|
20
|
+
[](https://github.com/m0squared/m0s-indicator)
|
|
21
|
+
[](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,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 };
|