vibe-usage-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/build/cli.js +197 -0
- package/build/core/claude-processes.d.ts +51 -0
- package/build/core/claude-processes.js +288 -0
- package/build/core/claude-profile.d.ts +2 -0
- package/build/core/claude-profile.js +146 -0
- package/build/core/claude-renew.d.ts +12 -0
- package/build/core/claude-renew.js +79 -0
- package/build/core/continuity.d.ts +90 -0
- package/build/core/continuity.js +1244 -0
- package/build/core/diagnostics.d.ts +25 -0
- package/build/core/diagnostics.js +229 -0
- package/build/core/environment.d.ts +12 -0
- package/build/core/environment.js +25 -0
- package/build/core/errors.d.ts +23 -0
- package/build/core/errors.js +48 -0
- package/build/core/files.d.ts +20 -0
- package/build/core/files.js +136 -0
- package/build/core/glance.d.ts +25 -0
- package/build/core/glance.js +107 -0
- package/build/core/handover.d.ts +61 -0
- package/build/core/handover.js +333 -0
- package/build/core/history.d.ts +19 -0
- package/build/core/history.js +134 -0
- package/build/core/launch.d.ts +78 -0
- package/build/core/launch.js +256 -0
- package/build/core/measurements.d.ts +11 -0
- package/build/core/measurements.js +75 -0
- package/build/core/model-families.d.ts +12 -0
- package/build/core/model-families.js +61 -0
- package/build/core/model.d.ts +32 -0
- package/build/core/model.js +86 -0
- package/build/core/models.d.ts +25 -0
- package/build/core/models.js +164 -0
- package/build/core/redact.d.ts +2 -0
- package/build/core/redact.js +64 -0
- package/build/core/selection.d.ts +65 -0
- package/build/core/selection.js +173 -0
- package/build/core/service.d.ts +35 -0
- package/build/core/service.js +484 -0
- package/build/core/sessions.d.ts +87 -0
- package/build/core/sessions.js +868 -0
- package/build/core/settings.d.ts +16 -0
- package/build/core/settings.js +377 -0
- package/build/core/tray-icon.d.ts +5 -0
- package/build/core/tray-icon.js +91 -0
- package/build/core/updater.d.ts +29 -0
- package/build/core/updater.js +88 -0
- package/build/core/updates.d.ts +5 -0
- package/build/core/updates.js +29 -0
- package/build/launchers/discover-antigravity.ps1 +68 -0
- package/build/launchers/terminal.ps1 +78 -0
- package/build/main/main.js +587 -0
- package/build/providers/antigravity.d.ts +48 -0
- package/build/providers/antigravity.js +463 -0
- package/build/providers/claude-keychain.d.ts +13 -0
- package/build/providers/claude-keychain.js +42 -0
- package/build/providers/claude.d.ts +33 -0
- package/build/providers/claude.js +218 -0
- package/build/providers/codex.d.ts +21 -0
- package/build/providers/codex.js +221 -0
- package/build/providers/common.d.ts +63 -0
- package/build/providers/common.js +294 -0
- package/build/providers/cursor.d.ts +14 -0
- package/build/providers/cursor.js +141 -0
- package/build/providers/profiles.d.ts +10 -0
- package/build/providers/profiles.js +69 -0
- package/build/providers/tls-trust.d.ts +7 -0
- package/build/providers/tls-trust.js +29 -0
- package/build/providers/types.d.ts +19 -0
- package/build/providers/types.js +1 -0
- package/build/shared/account-state.d.ts +19 -0
- package/build/shared/account-state.js +75 -0
- package/build/shared/catalog.d.ts +85 -0
- package/build/shared/catalog.js +125 -0
- package/build/shared/format.d.ts +40 -0
- package/build/shared/format.js +134 -0
- package/build/shared/i18n/index.d.ts +12 -0
- package/build/shared/i18n/index.js +40 -0
- package/build/shared/i18n/locales/de.d.ts +2 -0
- package/build/shared/i18n/locales/de.js +328 -0
- package/build/shared/i18n/locales/en.d.ts +328 -0
- package/build/shared/i18n/locales/en.js +330 -0
- package/build/shared/i18n/locales/es.d.ts +2 -0
- package/build/shared/i18n/locales/es.js +328 -0
- package/build/shared/i18n/locales/fr.d.ts +2 -0
- package/build/shared/i18n/locales/fr.js +328 -0
- package/build/shared/i18n/locales/it.d.ts +2 -0
- package/build/shared/i18n/locales/it.js +327 -0
- package/build/shared/i18n/locales/ja.d.ts +2 -0
- package/build/shared/i18n/locales/ja.js +322 -0
- package/build/shared/i18n/locales/ko.d.ts +2 -0
- package/build/shared/i18n/locales/ko.js +322 -0
- package/build/shared/i18n/locales/nl.d.ts +2 -0
- package/build/shared/i18n/locales/nl.js +327 -0
- package/build/shared/i18n/locales/pl.d.ts +2 -0
- package/build/shared/i18n/locales/pl.js +332 -0
- package/build/shared/i18n/locales/pt.d.ts +2 -0
- package/build/shared/i18n/locales/pt.js +328 -0
- package/build/shared/i18n/locales/ru.d.ts +2 -0
- package/build/shared/i18n/locales/ru.js +331 -0
- package/build/shared/i18n/locales/tr.d.ts +2 -0
- package/build/shared/i18n/locales/tr.js +322 -0
- package/build/shared/i18n/locales/zh.d.ts +2 -0
- package/build/shared/i18n/locales/zh.js +322 -0
- package/build/shared/i18n/messages.d.ts +10 -0
- package/build/shared/i18n/messages.js +1 -0
- package/build/shared/ipc.d.ts +138 -0
- package/build/shared/ipc.js +2 -0
- package/build/shared/tsconfig.tsbuildinfo +1 -0
- package/build/shared/types.d.ts +205 -0
- package/build/shared/types.js +1 -0
- package/package.json +144 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mustafa Çağrı Güven
|
|
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
CHANGED
|
@@ -1,3 +1,276 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** · [Türkçe](docs/readme/README.tr.md) · [Deutsch](docs/readme/README.de.md) · [Español](docs/readme/README.es.md) · [Français](docs/readme/README.fr.md) · [Italiano](docs/readme/README.it.md) · [Português](docs/readme/README.pt.md) · [Nederlands](docs/readme/README.nl.md) · [Polski](docs/readme/README.pl.md) · [日本語](docs/readme/README.ja.md) · [한국어](docs/readme/README.ko.md) · [简体中文](docs/readme/README.zh.md) · [Русский](docs/readme/README.ru.md)
|
|
4
|
+
|
|
5
|
+
<img src="assets/icon.png" width="96" alt="" />
|
|
6
|
+
|
|
7
|
+
# Vibe Usage
|
|
8
|
+
|
|
9
|
+
**Every AI coding subscription you pay for, in one menu-bar window.**
|
|
10
|
+
|
|
11
|
+
See how much quota Claude Code, Codex, Cursor and Antigravity have left, and keep working when a limit runs out:
|
|
12
|
+
your session continues on another account or tool, without re-explaining the task.
|
|
13
|
+
|
|
14
|
+
Windows tray · macOS menu bar · 13 languages · no passwords, no tokens stored
|
|
15
|
+
|
|
16
|
+
**[Download for Windows and macOS](https://github.com/mustafacagri/vibe-usage/releases/latest)** · Terminal: `npm install -g vibe-usage-cli`
|
|
17
|
+
|
|
18
|
+
<img src="docs/media/demo.gif" width="400" alt="Vibe Usage walkthrough: switching accounts, every account side by side, the compact view, Smart Launch and Handover" />
|
|
19
|
+
|
|
20
|
+
<sub>[Watch in full resolution (MP4)](docs/media/demo.mp4)</sub>
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
If you code with several AI tools, you hit the same wall over and over: the 5-hour limit runs out mid-task, you open
|
|
27
|
+
another tool, and you start explaining the task from scratch. Each provider shows its usage somewhere different, if at
|
|
28
|
+
all.
|
|
29
|
+
|
|
30
|
+
Vibe Usage reads the sessions already signed in on your machine and shows every limit side by side: how much is left
|
|
31
|
+
and when it resets. When one tool is running low, it starts the next one with verified quota in your project folder,
|
|
32
|
+
and writes a briefing of the current session so the new tool can continue where the old one stopped.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
<table>
|
|
37
|
+
<tr>
|
|
38
|
+
<td width="50%" valign="top">
|
|
39
|
+
|
|
40
|
+
### Quota at a glance
|
|
41
|
+
|
|
42
|
+
Every limit of every account: 5-hour and weekly windows, per-model scopes, spend pools. Countdowns show when each one
|
|
43
|
+
resets, and a limit that will run out before it resets says when, at the current pace. The last 7 days are one click
|
|
44
|
+
away as a chart. If a refresh fails, the last reading stays visible and is clearly marked as stale. Unknown numbers are
|
|
45
|
+
never shown as a full quota. The tray icon is a ring that fills with what your first tool has left; hover it for every
|
|
46
|
+
tool. Show each tool's accounts side by side, order the tools as you like, and switch to a compact view with one line
|
|
47
|
+
per account.
|
|
48
|
+
|
|
49
|
+
</td>
|
|
50
|
+
<td width="50%" valign="top">
|
|
51
|
+
|
|
52
|
+
<img src="docs/media/main.png" width="100%" alt="Quota cards for Claude, Codex, Cursor and Antigravity, with each tool’s accounts side by side" />
|
|
53
|
+
|
|
54
|
+
<img src="docs/media/compact.png" width="100%" alt="The compact view: one line per account" />
|
|
55
|
+
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<td valign="top">
|
|
60
|
+
|
|
61
|
+
### Keep working when a limit runs out
|
|
62
|
+
|
|
63
|
+
When Claude Code or Codex stops because its account hit the usage limit, the session continues where there is room:
|
|
64
|
+
on the same tool's account with the most quota left, then, if you allow it, on the next tool in your fallback order.
|
|
65
|
+
Opt in and it happens on its own; otherwise it is one click. The trigger is the tool's own "usage limit reached"
|
|
66
|
+
record, never a percentage, so two agents never work on the same folder at once. A Claude Code session moves to your
|
|
67
|
+
other Claude account **whole** and goes on by itself; other tools get a handover brief (kept out of `git status`).
|
|
68
|
+
An account running low offers the same move early, and warns if its session is still running. When the first account
|
|
69
|
+
has quota again, bringing the work back is one click.
|
|
70
|
+
|
|
71
|
+
If the computer goes down (a restart, a crash, a power cut) while Claude Code sessions are open, they open again where
|
|
72
|
+
they left off, on the account they belong to, and a turn that was cut off goes on. Suggested by default, or on its own
|
|
73
|
+
if you choose. Sessions you closed yourself stay closed.
|
|
74
|
+
|
|
75
|
+
### Smart Launch
|
|
76
|
+
|
|
77
|
+
Picks the first tool in your priority order whose quota is **verified** to have room, then opens it in a new terminal
|
|
78
|
+
in the project folder you choose. If no tool can be verified, it says so instead of guessing.
|
|
79
|
+
|
|
80
|
+
</td>
|
|
81
|
+
<td valign="top">
|
|
82
|
+
|
|
83
|
+
<img src="docs/media/smart-launch.png" width="100%" alt="Smart Launch recommending Codex because Claude's 5-hour limit is nearly spent" />
|
|
84
|
+
|
|
85
|
+
</td>
|
|
86
|
+
</tr>
|
|
87
|
+
<tr>
|
|
88
|
+
<td valign="top">
|
|
89
|
+
|
|
90
|
+
### Handover
|
|
91
|
+
|
|
92
|
+
Choose a session from Claude Code, Codex, Cursor Agent or Antigravity. Vibe Usage summarizes it into
|
|
93
|
+
`VIBE_HANDOVER.md` in the project folder: the goal, the latest request, the recent conversation, the files touched and
|
|
94
|
+
the git status. **Preview brief** shows it before anything is written or started; recognizable secrets are redacted and
|
|
95
|
+
the brief is marked as unverified context. Handing over then uses exactly the previewed session and starts the target
|
|
96
|
+
tool with a prompt to read the brief. Only sessions started in the chosen folder are listed, and a file of your own
|
|
97
|
+
named `VIBE_HANDOVER.md` is never overwritten.
|
|
98
|
+
|
|
99
|
+
</td>
|
|
100
|
+
<td valign="top">
|
|
101
|
+
|
|
102
|
+
<img src="docs/media/handover.png" width="100%" alt="Handover dialog moving a Claude session to Codex" />
|
|
103
|
+
|
|
104
|
+
</td>
|
|
105
|
+
</tr>
|
|
106
|
+
<tr>
|
|
107
|
+
<td valign="top">
|
|
108
|
+
|
|
109
|
+
### Multiple accounts
|
|
110
|
+
|
|
111
|
+
Claude and Codex keep one sign-in per profile folder. **Add account** creates a new folder and opens the tool's sign-in
|
|
112
|
+
there; `~/.claude-*` and `~/.codex-*` folders you made yourself are found automatically. Each account is shown by the
|
|
113
|
+
e-mail it is signed in with, and every signed-in Antigravity account is tracked separately too. Pick the primary
|
|
114
|
+
account, launch Claude Code with a specific account, and set the fallback order and the preferred model per tool.
|
|
115
|
+
A sign-in not finished within ten minutes takes the account off again, and the next account reuses its folder.
|
|
116
|
+
|
|
117
|
+
</td>
|
|
118
|
+
<td valign="top">
|
|
119
|
+
|
|
120
|
+
<img src="docs/media/settings.png" width="100%" alt="Settings: connected accounts, two each for Claude and Codex" />
|
|
121
|
+
|
|
122
|
+
</td>
|
|
123
|
+
</tr>
|
|
124
|
+
</table>
|
|
125
|
+
|
|
126
|
+
### Per-project choices, history and diagnostics
|
|
127
|
+
|
|
128
|
+
- **Project preferences** (Settings): give a folder its own tool, model, account and automatic continuation. They narrow
|
|
129
|
+
what Smart Launch and continuation pick, never replace the verified-quota check, and a project set to *off* stays off
|
|
130
|
+
even when continuation is on globally.
|
|
131
|
+
- **Notifications**: choose the low-quota line and quiet hours (they may cross midnight). Quiet hours skip notifications
|
|
132
|
+
(they are not sent later); quota is still read, work still continues and suggestions wait in the window.
|
|
133
|
+
- **Handover history** lists each continuation with its project, route and outcome. A failed one can be tried again,
|
|
134
|
+
checked against current quota first.
|
|
135
|
+
- **Diagnostics** shows, per tool, whether it is installed, each account's state and freshness, and the next step.
|
|
136
|
+
**Copy report** gives a version without names, e-mail addresses or folders, ready for an issue.
|
|
137
|
+
- **Backup and restore**: export, import or reset the settings. Every save keeps the previous good copy; a damaged
|
|
138
|
+
settings file is restored from it (and kept aside), and a file written by a newer version is never overwritten.
|
|
139
|
+
|
|
140
|
+
## Supported tools
|
|
141
|
+
|
|
142
|
+
| Tool | Where the quota comes from | Sessions for handover | Launch |
|
|
143
|
+
| ----------- | ----------------------------------------------------------------------- | --------------------------------------- | -------------------------------------- |
|
|
144
|
+
| Claude Code | The profile's OAuth usage endpoint (Keychain on macOS) | `~/.claude/projects` | `claude` CLI with the chosen profile |
|
|
145
|
+
| Codex | The installed CLI's app-server rate-limit methods | `~/.codex/sessions` | `codex` CLI |
|
|
146
|
+
| Cursor | The signed-in session's usage summary | Cursor Agent CLI chats (`~/.cursor/chats`) | `cursor-agent`, otherwise the app |
|
|
147
|
+
| Antigravity | The running app's local service (process and port ownership verified) | `~/.gemini/antigravity-ide` | Antigravity app |
|
|
148
|
+
|
|
149
|
+
Model lists come from each tool's own data: Claude Code aliases and the models you used, the Codex model cache, and
|
|
150
|
+
`cursor-agent --list-models` (cached for 24 hours).
|
|
151
|
+
|
|
152
|
+
## Privacy and safety
|
|
153
|
+
|
|
154
|
+
- **Your sign-ins stay as they are.** It uses the sessions that already exist. It never asks for a password, never
|
|
155
|
+
reads or stores a refresh token, never signs you out, and never touches running work. When work may move between
|
|
156
|
+
accounts on its own, an expired Claude Code sign-in is renewed by Claude Code itself: one minimal Haiku request (one
|
|
157
|
+
word, no tools, nothing saved), at most every 30 minutes per account. Continuing a Claude Code session on
|
|
158
|
+
another account copies its transcript into that account's profile folder; the original is left as it is.
|
|
159
|
+
- **Few endpoints.** Quota requests go only to the Anthropic and Cursor usage endpoints, with TLS verification on
|
|
160
|
+
(and the renewal above goes through Claude Code).
|
|
161
|
+
Codex and Antigravity are read locally. Once a day the app asks GitHub for its latest release (Settings → General
|
|
162
|
+
turns this off). An update is downloaded only when you click **Download**, only from this repository's releases, and
|
|
163
|
+
checked against the release's SHA-512 checksums before **Restart to update** installs it.
|
|
164
|
+
- **Nothing secret on disk.** Settings live in `%APPDATA%\Vibe Usage\settings.json` (Windows) or
|
|
165
|
+
`~/Library/Application Support/Vibe Usage/settings.json` (macOS), with `settings.json.bak` (the previous good copy)
|
|
166
|
+
and, if the file was ever damaged, `settings.json.damaged-<time>`. Next to them, `history.json` keeps a week of quota
|
|
167
|
+
percentages for the chart and the forecast (numbers only), `measurements.json` each account's last good reading (its
|
|
168
|
+
limits, percentages, reset times and plan, shown as stale after a restart until a fresh read), and `continuity.json`
|
|
169
|
+
the handover history: project folders, tool and account ids, session ids and outcomes, never conversation text,
|
|
170
|
+
tokens or e-mails.
|
|
171
|
+
- **A locked-down app.** The window is sandboxed with a strict CSP, cannot navigate or open other windows, and only
|
|
172
|
+
answers its own page. Electron fuses disable `runAsNode`, `NODE_OPTIONS` and the inspector, and enforce ASAR
|
|
173
|
+
integrity.
|
|
174
|
+
|
|
175
|
+
Found a security issue? Please report it privately as described in [SECURITY.md](SECURITY.md).
|
|
176
|
+
|
|
177
|
+
## Install
|
|
178
|
+
|
|
179
|
+
### Windows
|
|
180
|
+
|
|
181
|
+
Download `Vibe-Usage-<version>-win-setup.exe` from the [latest release](https://github.com/mustafacagri/vibe-usage/releases/latest) and
|
|
182
|
+
run it: it installs for your user only (no administrator rights), and once releases are signed it updates itself from
|
|
183
|
+
inside the app. Or take the
|
|
184
|
+
portable `Vibe-Usage-<version>-win-x64.zip` (or `-arm64`), unzip it and run `Vibe Usage.exe`; a portable copy
|
|
185
|
+
links to the release page when there is a new version. Either way it lives in the system tray.
|
|
186
|
+
|
|
187
|
+
Unless a release says it is signed, the app is not code-signed, so on first launch Windows SmartScreen may say **Windows protected your PC**. Click **More info**, then **Run anyway**.
|
|
188
|
+
|
|
189
|
+
### macOS
|
|
190
|
+
|
|
191
|
+
Download `Vibe-Usage-<version>-mac-arm64.dmg` (Apple silicon) from the
|
|
192
|
+
[latest release](https://github.com/mustafacagri/vibe-usage/releases/latest), open it and drag **Vibe Usage** to
|
|
193
|
+
Applications. The app is not signed with an Apple Developer ID yet, so Gatekeeper blocks the first launch: open
|
|
194
|
+
**System Settings › Privacy & Security** and click **Open Anyway**, or run:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
xattr -dr com.apple.quarantine "/Applications/Vibe Usage.app"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
On an Intel Mac, or to skip that step, build it yourself. You only need Node.js 24:
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
git clone https://github.com/mustafacagri/vibe-usage.git && cd vibe-usage
|
|
204
|
+
npm ci
|
|
205
|
+
npm run build:mac
|
|
206
|
+
cp -R "release/mac-arm64/Vibe Usage.app" /Applications/
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
On an Intel Mac the folder is `release/mac`. An app you build yourself is not quarantined and opens directly.
|
|
210
|
+
|
|
211
|
+
macOS lets only a signed app replace itself, so until releases are signed a new version is installed the same way
|
|
212
|
+
(the app links to it when one is out).
|
|
213
|
+
|
|
214
|
+
## Command line
|
|
215
|
+
|
|
216
|
+
The same data is available in the terminal on Windows and macOS, with or without the desktop app. It needs Node.js 24
|
|
217
|
+
or later and reads the same signed-in sessions as the app; when both are installed they share settings and language.
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
npm install -g vibe-usage-cli
|
|
221
|
+
vibe-usage status
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
<img src="docs/media/cli.png" width="640" alt="vibe-usage status in a terminal: every window of two Claude, two Codex, one Cursor and two Antigravity accounts" />
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
vibe-usage status [--json] # every account's quota once; --json for scripts
|
|
228
|
+
vibe-usage watch # the same, refreshed until Ctrl+C
|
|
229
|
+
vibe-usage run <claude|codex|cursor> [--account ID] [--cwd PATH] [--model MODEL] [--dry-run]
|
|
230
|
+
vibe-usage handover [--from TOOL] [--to TOOL] [--session ID] [--cwd PATH]
|
|
231
|
+
vibe-usage help
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
To try it without installing, run `npx vibe-usage-cli status`.
|
|
235
|
+
|
|
236
|
+
`run` starts the tool in this terminal with an account whose quota is verified; otherwise name one with `--account`
|
|
237
|
+
(the IDs are in `status --json`). `--dry-run` prints the account and the command without starting anything.
|
|
238
|
+
`handover` writes `VIBE_HANDOVER.md` for the folder's latest session (or `--session`) and opens the target tool with
|
|
239
|
+
it.
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
TypeScript in strict mode on Node.js 24 (the version Electron 44 embeds). Tests, the CLI and scripts run straight from
|
|
244
|
+
the `.ts` sources; the app is compiled to `build/`.
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
npm ci
|
|
248
|
+
npm start # build and run the app
|
|
249
|
+
npm test # unit and integration tests
|
|
250
|
+
npm run test:coverage # the same, with the coverage gates CI enforces
|
|
251
|
+
npm run lint # ESLint with type information
|
|
252
|
+
npm run format # Prettier
|
|
253
|
+
npm run ui-check # renders the built UI in all 13 languages and checks that every text fits
|
|
254
|
+
npm run media # regenerates the screenshots and the walkthrough in docs/media (needs ffmpeg)
|
|
255
|
+
npm run build:win # or build:mac
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Folder | Runs in | Contains |
|
|
259
|
+
| ----------------------------------------------------- | ------------- | ------------------------------------------------------ |
|
|
260
|
+
| `src/shared` | anywhere | provider catalog, data types, IPC contract, i18n |
|
|
261
|
+
| `src/ui` | the window | the interface |
|
|
262
|
+
| `src/core`, `src/providers`, `src/main`, `src/cli.ts` | Node/Electron | service, provider adapters, main process, CLI |
|
|
263
|
+
| `test`, `scripts` | Node | type-checked only, never compiled |
|
|
264
|
+
|
|
265
|
+
`VIBE_DATA_DIR` moves the settings folder. `VIBE_CLAUDE_BINARY`, `VIBE_CODEX_BINARY` and `VIBE_CURSOR_AGENT_BINARY`
|
|
266
|
+
point at specific CLI binaries. Tests never start a real tool or read real accounts.
|
|
267
|
+
|
|
268
|
+
## Limitations
|
|
269
|
+
|
|
270
|
+
- The macOS build is not notarized (see [macOS](#macos)).
|
|
271
|
+
- Antigravity's session model comes from an undocumented local record; if it cannot be read, the model is left blank.
|
|
272
|
+
- Cursor IDE (Composer) chats cannot be handed over; only Cursor Agent CLI sessions are read.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
[MIT](LICENSE)
|
package/build/cli.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Terminal front end: quota status, a live watch, running a provider CLI with verified quota,
|
|
3
|
+
// and handing a task over. Uses the desktop app's settings and language.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { applyEnvironment, environmentChanges } from "./core/environment.js";
|
|
8
|
+
import { AppError } from "./core/errors.js";
|
|
9
|
+
import { executeHandover, parseProviderChoice } from "./core/handover.js";
|
|
10
|
+
import { launchPlan, launchWarnings, projectDirectory } from "./core/launch.js";
|
|
11
|
+
import { selectAccount } from "./core/selection.js";
|
|
12
|
+
import { createService } from "./core/service.js";
|
|
13
|
+
import { appDataDir, settingsPath } from "./core/settings.js";
|
|
14
|
+
import { AUTO, isProviderId, providerById } from "./shared/catalog.js";
|
|
15
|
+
import { describeCode, describeWindow, formatAccountName, formatFreshness, LAUNCH_MESSAGES, toolLabel } from "./shared/format.js";
|
|
16
|
+
import { DEFAULT_LANGUAGE, translator } from "./shared/i18n/index.js";
|
|
17
|
+
const CLEAR_SCREEN = '\x1b[2J\x1b[H';
|
|
18
|
+
const EXIT_FAILURE = 1;
|
|
19
|
+
const EXIT_INTERRUPTED = 130;
|
|
20
|
+
// Strict: an unknown flag or a missing value is an error, never ignored.
|
|
21
|
+
function parseFlags(args, options) {
|
|
22
|
+
try {
|
|
23
|
+
return parseArgs({ args, options, strict: true, allowPositionals: false }).values;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new AppError('invalid_argument', {}, { cause: error });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function renderSnapshot(snapshot, context) {
|
|
30
|
+
const { t } = context;
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const provider of snapshot.providers) {
|
|
33
|
+
lines.push(providerById(provider.id).name.toUpperCase());
|
|
34
|
+
for (const account of provider.accounts) {
|
|
35
|
+
const name = `${account.primary ? '★ ' : ''}${formatAccountName(account, t)}`;
|
|
36
|
+
const freshness = formatFreshness(account.fetchedAt, account.status, context);
|
|
37
|
+
lines.push(` ${[name, account.plan, freshness].filter(Boolean).join(' · ')}`);
|
|
38
|
+
for (const window of account.windows) {
|
|
39
|
+
const view = describeWindow(window, context);
|
|
40
|
+
const scope = window.scope ? ` / ${window.scope}` : '';
|
|
41
|
+
lines.push(` ${view.label}${scope}: ${[view.value, view.reset, view.detail].filter(Boolean).join(' · ')}`);
|
|
42
|
+
}
|
|
43
|
+
if (account.messageCode)
|
|
44
|
+
lines.push(` ${describeCode(account.messageCode, t)}`);
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n').trimEnd();
|
|
49
|
+
}
|
|
50
|
+
function watch({ service, print }, t, locale) {
|
|
51
|
+
const draw = (snapshot) => {
|
|
52
|
+
// Only the live view says how to leave it.
|
|
53
|
+
print(`${process.stdout.isTTY ? CLEAR_SCREEN : ''}${renderSnapshot(snapshot, { t, locale })}\n\n${t('cliWatchFooter')}`);
|
|
54
|
+
};
|
|
55
|
+
draw(service.snapshot());
|
|
56
|
+
return service.refresh().then(snapshot => {
|
|
57
|
+
draw(snapshot);
|
|
58
|
+
let busy = false;
|
|
59
|
+
const timer = setInterval(() => {
|
|
60
|
+
if (busy)
|
|
61
|
+
return;
|
|
62
|
+
busy = true;
|
|
63
|
+
void service
|
|
64
|
+
.refresh()
|
|
65
|
+
.then(draw, () => undefined)
|
|
66
|
+
.finally(() => {
|
|
67
|
+
busy = false;
|
|
68
|
+
});
|
|
69
|
+
}, service.snapshot().settings.intervalSeconds * 1000);
|
|
70
|
+
// Runs until interrupted.
|
|
71
|
+
return new Promise(resolve => {
|
|
72
|
+
process.once('SIGINT', () => {
|
|
73
|
+
clearInterval(timer);
|
|
74
|
+
resolve(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Runs a provider CLI in this terminal for the chosen (or best eligible) account.
|
|
80
|
+
async function run(context, [providerId, ...args], t) {
|
|
81
|
+
if (!isProviderId(providerId))
|
|
82
|
+
throw new AppError('invalid_provider');
|
|
83
|
+
const flags = parseFlags(args, {
|
|
84
|
+
account: { type: 'string' },
|
|
85
|
+
cwd: { type: 'string' },
|
|
86
|
+
model: { type: 'string' },
|
|
87
|
+
'dry-run': { type: 'boolean' }
|
|
88
|
+
});
|
|
89
|
+
const snapshot = await context.service.refresh(providerId);
|
|
90
|
+
// Without an explicit --account, only an account with verified quota is used.
|
|
91
|
+
const chosen = selectAccount(snapshot, providerId, { accountId: flags.account, model: flags.model });
|
|
92
|
+
if (!chosen)
|
|
93
|
+
throw new AppError('no_verified_quota');
|
|
94
|
+
const cwd = await projectDirectory(path.resolve(context.cwd, flags.cwd ?? '.'));
|
|
95
|
+
const plan = await launchPlan(providerId, { account: chosen, model: flags.model });
|
|
96
|
+
if (!plan)
|
|
97
|
+
throw new AppError('no_cli', { tool: providerById(providerId).name });
|
|
98
|
+
const warnings = await launchWarnings(snapshot, chosen, { projectDir: cwd, model: flags.model });
|
|
99
|
+
const messages = warnings.map(w => t(w.key, w.params));
|
|
100
|
+
if (flags['dry-run']) {
|
|
101
|
+
const command = [plan.binary, ...plan.args];
|
|
102
|
+
context.print(JSON.stringify({ provider: providerId, accountId: chosen.id, projectDir: cwd, command, warnings: messages }, null, 2));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
for (const message of messages)
|
|
106
|
+
context.print(message);
|
|
107
|
+
return context.runAttached(plan.binary, plan.args, { cwd, env: applyEnvironment(environmentChanges(plan.env)) });
|
|
108
|
+
}
|
|
109
|
+
async function handover(context, args, t) {
|
|
110
|
+
const flags = parseFlags(args, {
|
|
111
|
+
from: { type: 'string' },
|
|
112
|
+
to: { type: 'string' },
|
|
113
|
+
cwd: { type: 'string' },
|
|
114
|
+
session: { type: 'string' }
|
|
115
|
+
});
|
|
116
|
+
const result = await executeHandover({
|
|
117
|
+
projectDir: path.resolve(context.cwd, flags.cwd ?? '.'),
|
|
118
|
+
fromProvider: parseProviderChoice(flags.from ?? AUTO),
|
|
119
|
+
toProvider: parseProviderChoice(flags.to ?? AUTO),
|
|
120
|
+
sessionId: flags.session ?? AUTO
|
|
121
|
+
}, await context.service.refresh(), { dataDir: context.dataDir, ...context.launch });
|
|
122
|
+
context.print(t(LAUNCH_MESSAGES[result.mode], { tool: toolLabel(result.to, result.model) }));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
// Runs one command and resolves with the process exit code. Failures are printed in the
|
|
126
|
+
// user's language; nothing else reaches the terminal.
|
|
127
|
+
export async function runCli([command = 'status', ...args], context) {
|
|
128
|
+
const { language } = context.service.snapshot().settings;
|
|
129
|
+
const t = translator(language);
|
|
130
|
+
try {
|
|
131
|
+
switch (command) {
|
|
132
|
+
case 'help':
|
|
133
|
+
case '--help':
|
|
134
|
+
context.print(t('cliHelp'));
|
|
135
|
+
return 0;
|
|
136
|
+
case 'status': {
|
|
137
|
+
const { json } = parseFlags(args, { json: { type: 'boolean' } });
|
|
138
|
+
const snapshot = await context.service.refresh();
|
|
139
|
+
context.print(json ? JSON.stringify(snapshot, null, 2) : renderSnapshot(snapshot, { t, locale: language }));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
case 'watch':
|
|
143
|
+
return await watch(context, t, language);
|
|
144
|
+
case 'run':
|
|
145
|
+
return await run(context, args, t);
|
|
146
|
+
case 'handover':
|
|
147
|
+
return await handover(context, args, t);
|
|
148
|
+
default:
|
|
149
|
+
throw new AppError('unknown_command');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const { code, params } = error instanceof AppError ? error : new AppError('unexpected');
|
|
154
|
+
context.printError(describeCode(code, t, params));
|
|
155
|
+
return EXIT_FAILURE;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// The tool shares this terminal: Ctrl+C reaches it as well, and it decides what to do with it.
|
|
159
|
+
// This process waits for it rather than exiting under it and leaving it attached.
|
|
160
|
+
function runAttached(binary, args, options) {
|
|
161
|
+
const ignore = () => undefined;
|
|
162
|
+
process.on('SIGINT', ignore);
|
|
163
|
+
return new Promise(resolve => {
|
|
164
|
+
const child = spawn(binary, args, { ...options, shell: false, stdio: 'inherit' });
|
|
165
|
+
child.once('error', () => {
|
|
166
|
+
resolve(EXIT_FAILURE);
|
|
167
|
+
});
|
|
168
|
+
child.once('close', (code, signal) => {
|
|
169
|
+
resolve(code ?? (signal === 'SIGINT' ? EXIT_INTERRUPTED : EXIT_FAILURE));
|
|
170
|
+
});
|
|
171
|
+
}).finally(() => {
|
|
172
|
+
process.off('SIGINT', ignore);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Runs only as a program; importing the module (tests) has no side effects.
|
|
176
|
+
if (import.meta.main) {
|
|
177
|
+
const dataDir = appDataDir();
|
|
178
|
+
createService({ settingsPath: settingsPath(dataDir) })
|
|
179
|
+
.then(service => runCli(process.argv.slice(2), {
|
|
180
|
+
service,
|
|
181
|
+
dataDir,
|
|
182
|
+
cwd: process.cwd(),
|
|
183
|
+
print: text => {
|
|
184
|
+
console.log(text);
|
|
185
|
+
},
|
|
186
|
+
printError: text => {
|
|
187
|
+
console.error(text);
|
|
188
|
+
},
|
|
189
|
+
runAttached
|
|
190
|
+
}))
|
|
191
|
+
.then(code => {
|
|
192
|
+
process.exitCode = code;
|
|
193
|
+
}, (error) => {
|
|
194
|
+
console.error(describeCode(error instanceof AppError ? error.code : 'unexpected', translator(DEFAULT_LANGUAGE)));
|
|
195
|
+
process.exitCode = EXIT_FAILURE;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const CLAUDE_SESSION_STATUSES: readonly ["busy", "idle", "shell", "waiting"];
|
|
2
|
+
export type ClaudeSessionStatus = (typeof CLAUDE_SESSION_STATUSES)[number];
|
|
3
|
+
export interface LiveClaudeSession {
|
|
4
|
+
pid: number;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
listed: boolean;
|
|
7
|
+
status: ClaudeSessionStatus | null;
|
|
8
|
+
statusSince: number;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
procStart: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ProcessStart {
|
|
13
|
+
at: number;
|
|
14
|
+
ticks: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface SystemProcess extends ProcessStart {
|
|
17
|
+
pid: number;
|
|
18
|
+
command: string;
|
|
19
|
+
}
|
|
20
|
+
export type ProcessLookup = {
|
|
21
|
+
kind: 'found';
|
|
22
|
+
process: LiveClaudeSession;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'none';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'unknown';
|
|
27
|
+
reason: string;
|
|
28
|
+
};
|
|
29
|
+
export type ProcessEnd = 'ended' | 'gone' | 'other' | 'unknown';
|
|
30
|
+
export interface ProcessControl {
|
|
31
|
+
isAlive: (pid: number) => boolean;
|
|
32
|
+
started: (pid: number) => Promise<ProcessStart | null>;
|
|
33
|
+
end: (pid: number) => void;
|
|
34
|
+
list?: () => Promise<SystemProcess[]>;
|
|
35
|
+
}
|
|
36
|
+
export declare function parseWindowsStart(stdout: string): ProcessStart | null;
|
|
37
|
+
export declare function parseWindowsProcesses(stdout: string): SystemProcess[];
|
|
38
|
+
export declare function parsePsProcesses(stdout: string): {
|
|
39
|
+
pid: number;
|
|
40
|
+
command: string;
|
|
41
|
+
}[];
|
|
42
|
+
export declare function processStarted(pid: number): Promise<ProcessStart | null>;
|
|
43
|
+
export declare function resumingProcess(processes: readonly SystemProcess[], sessionId: string): SystemProcess | null;
|
|
44
|
+
export declare const systemProcesses: ProcessControl;
|
|
45
|
+
export interface OpenClaudeSession extends LiveClaudeSession {
|
|
46
|
+
cwd: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function openSessionsReader(processes?: Pick<ProcessControl, 'isAlive' | 'started'>): (configDir: string) => Promise<OpenClaudeSession[]>;
|
|
49
|
+
export declare function findClaudeProcess(configDir: string, sessionId: string, processes?: Pick<ProcessControl, 'isAlive' | 'list'>): Promise<ProcessLookup>;
|
|
50
|
+
export declare function isSameProcess(live: LiveClaudeSession, started: ProcessStart): boolean;
|
|
51
|
+
export declare function endClaudeSession(live: LiveClaudeSession, processes?: ProcessControl): Promise<ProcessEnd>;
|