gemiterm 2.1.0 → 2.2.0-beta.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.md +21 -0
- package/README.md +223 -154
- package/package.json +1 -1
- package/src/cli/commands/continue-command.ts +75 -1
- package/src/cli/commands/new-command.ts +62 -0
- package/src/cli/utils/long-arg-guard.ts +28 -0
- package/src/cli/utils/prompt-file.ts +26 -0
- package/src/infrastructure/io.ts +9 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GemiTerm contributors
|
|
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,268 +1,337 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# GemiTerm
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
### Google Gemini in your terminal — list, search, export, continue, and debate from the command line.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/gemiterm)
|
|
8
|
+
[](https://bun.sh)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](LICENSE.md)
|
|
11
|
+
[](#installation)
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
- **Chromium Browser** — GemiTerm uses your system Chrome/Edge if available, otherwise installs Playwright's Chromium automatically
|
|
9
|
-
- **Google Account** with access to [Gemini](https://gemini.google.com)
|
|
13
|
+
**List · Search · Export · Continue · Debate · Automate**
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
[Quick Start](#quick-start) · [Features](#features) · [Cross-Agent Debates](#cross-agent-debates-with-gemini) · [Commands](#commands) · [Configuration](#configuration) · [Contributing](#contributing) · [Acknowledgements](#acknowledgments)
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
</div>
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
bunx gemiterm auth # authenticate with Google
|
|
17
|
-
bunx gemiterm list # list your chats
|
|
18
|
-
```
|
|
19
|
+
---
|
|
19
20
|
|
|
20
|
-
##
|
|
21
|
+
## What is GemiTerm?
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
[Gemini](https://gemini.google.com) is one of the most capable AI assistants available — and almost all of your best conversations with it live **trapped inside the web UI**, with no public API to reach them. GemiTerm is the bridge.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
bun install gemiterm -g
|
|
26
|
-
```
|
|
25
|
+
It's a fast, scriptable command-line client for the **Google Gemini web app**. Authenticate once, then list, search, fetch, export, continue, and delete chats from your shell. Pipe JSON into other tools. Wire it into CI. Or hand it to your AI coding agent and let it argue trade-offs with Gemini in real time.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
GemiTerm is built with **[Bun](https://bun.sh)** and TypeScript, ships as a single native binary, and authenticates via Playwright — no Google API key, no rate limits, no quota.
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
gemiterm auth
|
|
32
|
-
gemiterm list
|
|
33
|
-
```
|
|
29
|
+
> **TL;DR:** If you've ever wanted to `grep` your Gemini history, bulk-export months of chats, or have your coding agent debate Gemini about architecture before you commit — GemiTerm is for you.
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
## Why GemiTerm?
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
- **No public API? No problem.** GemiTerm drives the real Gemini web app via a headless browser. It sees exactly what you see, including your entire history.
|
|
34
|
+
- **Cross-agent debates built-in.** Spin up a `debate-with-gemini` skill and your coding agent (Claude Code, OpenCode, GitHub Copilot, …) will argue both sides of a technical decision with real Gemini responses, for N rounds. Catch design flaws before they ship.
|
|
35
|
+
- **Scriptable & pipeable.** First-class `text` and `json` output. Works in cron jobs, Makefiles, shell loops, and CI.
|
|
36
|
+
- **Searchable archive.** Bulk-export to Markdown or JSON and run `grep`, `rg`, or your favorite search tool over months of AI conversations.
|
|
37
|
+
- **Multi-profile, multi-account.** Keep work, personal, and side-project identities separate. One CLI, many Gemini accounts.
|
|
38
|
+
- **Single binary, every OS.** Native builds for Windows, Linux, and macOS. No Node, no Python, no runtime to install.
|
|
39
|
+
- **Local-first, private.** Cookies never leave your machine. 7-day freshness window. You own your data.
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
## Features
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
- 🔍 **List & search** — find any Gemini chat by title, date, or content (`--sort`, `--after`, `--before`, `--search`, `--all`, `--all-profiles`)
|
|
44
|
+
- 📥 **Bulk export** — Markdown or JSON, indexed `index.md`, ready for grep and archival
|
|
45
|
+
- 💬 **Continue or start** — interactive REPL or one-shot, with `--prompt-file` for long prompts
|
|
46
|
+
- 🗑️ **Delete with confirmation** — clean up old chats safely (`--force` to skip)
|
|
47
|
+
- 👤 **Multi-profile** — separate identities for work / personal / side-projects, one-click switching
|
|
48
|
+
- 🤖 **AI-agent skills** — plug into OpenCode, Claude Code, GitHub Copilot, and any agent that reads skills
|
|
49
|
+
- 🗣️ **Cross-agent debates** — your agent argues both sides with Gemini, surfaces real agreements and disagreements
|
|
50
|
+
- 📦 **Single binary** — Bun-compiled, no runtime required; works on Windows, Linux, macOS
|
|
51
|
+
- 🔐 **Local cookies** — never uploaded; 7-day freshness window
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
## Cross-Agent Debates with Gemini
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
curl -fsSL https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.sh | bash
|
|
49
|
-
```
|
|
55
|
+
The killer feature isn't just CLI access — it's what your **AI coding agent** can do with it.
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
GemiTerm ships with a companion **skills package** (`opencode-gemiterm-skills`) that teaches any LLM-powered terminal how to use GemiTerm on demand. Two skills are included:
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
| Skill | What it does |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| **`gemiterm`** | Search, list, export, and manage your Gemini chats from inside an agent session. |
|
|
62
|
+
| **`debate-with-gemini`** | Run structured multi-turn technical debates with Gemini — perfect for stress-testing architecture decisions, trade-offs, and design choices. |
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
Install in one command:
|
|
56
65
|
|
|
57
66
|
```bash
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
bun run typecheck # TypeScript type checking
|
|
62
|
-
bun run build # compile to standalone Bun binary
|
|
63
|
-
bun run build:linux # cross-compile for Linux x64
|
|
64
|
-
bun run build:windows # cross-compile for Windows x64
|
|
67
|
+
gemiterm install-skills
|
|
68
|
+
# or, for any agent that supports the skills CLI
|
|
69
|
+
bunx opencode-gemiterm-skills install
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
bash scripts/install-browser.sh # Linux/macOS
|
|
70
|
-
pwsh scripts/install-browser.ps1 # Windows
|
|
71
|
-
```
|
|
72
|
+
Then in your agent session, just ask:
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
> **You:** "Debate Gemini for/against using SQLite as the primary database for a SaaS app. Context: docs/arch.md. 5 turns."
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
The agent reads your context, seeds a fresh Gemini chat with the opposing view, and runs a 5-round autonomous back-and-forth:
|
|
76
77
|
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
bun run build:release # minified release binary (dist/gemiterm)
|
|
78
|
+
```
|
|
79
|
+
Debate complete (5 turns). Gemini argued FOR SQLite (simplicity, zero-config).
|
|
80
|
+
I argued AGAINST (concurrency limits, no network access, scaling ceiling).
|
|
81
|
+
Key agreements: fine for prototyping, migrate to Postgres before 100+ concurrent users.
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
- **Linux/macOS**: `dist/gemiterm`
|
|
86
|
-
- **Windows**: `dist/gemiterm.exe`
|
|
84
|
+
Other use cases for `debate-with-gemini`:
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
- **Validate API design** — "Have Gemini argue against this REST contract. 3 turns."
|
|
87
|
+
- **Review a refactor** — "Stress-test the migration to event-sourcing with Gemini. 4 turns."
|
|
88
|
+
- **Compare libraries** — "Argue both sides of `Zod` vs. `valibot` for this schema. 3 turns."
|
|
89
|
+
- **Continue a previous debate** — "Resume chat `c_abc123` for 3 more turns."
|
|
89
90
|
|
|
90
|
-
The
|
|
91
|
-
- `GemiTerm` — Linux x64 binary
|
|
92
|
-
- `GemiTerm.exe` — Windows x64 binary
|
|
93
|
-
- `install.sh` — POSIX installer script
|
|
94
|
-
- `install.ps1` — Windows PowerShell installer script
|
|
91
|
+
The full skill bundle lives at [`opencode-gemiterm-skills`](https://www.npmjs.com/package/opencode-gemiterm-skills).
|
|
95
92
|
|
|
96
|
-
##
|
|
93
|
+
## Quick Start
|
|
97
94
|
|
|
98
|
-
###
|
|
95
|
+
### Try without installing
|
|
99
96
|
|
|
100
97
|
```bash
|
|
101
|
-
gemiterm auth
|
|
98
|
+
bunx gemiterm auth # sign in to Google
|
|
99
|
+
bunx gemiterm list # show your recent chats
|
|
102
100
|
```
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
### Check Status
|
|
102
|
+
### Install globally (recommended for daily use)
|
|
107
103
|
|
|
108
104
|
```bash
|
|
109
|
-
gemiterm
|
|
105
|
+
bun install -g gemiterm
|
|
106
|
+
|
|
107
|
+
gemiterm auth
|
|
108
|
+
gemiterm list
|
|
110
109
|
```
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
### Install scripts (no Bun required — bootstraps it for you)
|
|
113
112
|
|
|
114
|
-
|
|
113
|
+
**Windows (PowerShell 7+):**
|
|
115
114
|
|
|
116
|
-
```
|
|
117
|
-
|
|
115
|
+
```powershell
|
|
116
|
+
irm https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.ps1 | iex
|
|
118
117
|
```
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
- `-n, --limit N`: Maximum number of chats (default: 10)
|
|
122
|
-
- `--offset N`: Skip first N chats (default: 0)
|
|
123
|
-
- `--sort <recent|oldest|alpha>`: Sort order
|
|
124
|
-
- `-s, --search <query>`: Filter by title
|
|
125
|
-
- `--after <date>`: Only chats after this date
|
|
126
|
-
- `--before <date>`: Only chats before this date
|
|
127
|
-
- `--all`: Show all chats (no limit)
|
|
128
|
-
- `--all-profiles`: Merge chats from all profiles
|
|
129
|
-
- `-f, --format <text|json>`: Output format
|
|
130
|
-
- `-p, --path <path>`: Save output to file
|
|
131
|
-
|
|
132
|
-
### Fetch Chat History
|
|
119
|
+
**Linux / macOS:**
|
|
133
120
|
|
|
134
121
|
```bash
|
|
135
|
-
|
|
122
|
+
curl -fsSL https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.sh | bash
|
|
136
123
|
```
|
|
137
124
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
125
|
+
> **Upgrading from v1.4.1?** Your profiles, cookies, and default profile marker are preserved. See [docs/INSTALL.md](docs/INSTALL.md) for details.
|
|
126
|
+
|
|
127
|
+
See [docs/INSTALL.md](docs/INSTALL.md) for the full guide, uninstall instructions, troubleshooting, and build-from-source steps.
|
|
128
|
+
|
|
129
|
+
## Prerequisites
|
|
130
|
+
|
|
131
|
+
- **[Bun](https://bun.sh)** runtime ≥ 1.0.0 (only required for `bunx` / `bun install` use — not needed for install scripts)
|
|
132
|
+
- **Chromium browser** — GemiTerm prefers your system Chrome / Edge; otherwise it installs Playwright's Chromium automatically (`gemiterm install-browser`)
|
|
133
|
+
- **Google Account** with access to [Gemini](https://gemini.google.com)
|
|
141
134
|
|
|
142
|
-
|
|
135
|
+
## Commands
|
|
136
|
+
|
|
137
|
+
| Command | Alias | Purpose |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| `gemiterm auth` | `gemiterm login` | Sign in to Google and save cookies |
|
|
140
|
+
| `gemiterm status` | — | Show config dir and profile table |
|
|
141
|
+
| `gemiterm list` | — | List chats (filter, sort, search) |
|
|
142
|
+
| `gemiterm fetch <id>` | — | Fetch a chat's full history |
|
|
143
|
+
| `gemiterm new [message]` | — | Start a new chat (one-shot or REPL) |
|
|
144
|
+
| `gemiterm continue <id> [msg]` | — | Continue an existing chat |
|
|
145
|
+
| `gemiterm export <id>` | — | Export one chat to Markdown / JSON |
|
|
146
|
+
| `gemiterm export-all` | — | Bulk-export all chats with `index.md` |
|
|
147
|
+
| `gemiterm delete <id>` | — | Delete a chat (with confirmation) |
|
|
148
|
+
| `gemiterm profile <sub>` | — | Manage profiles (list / add / delete / rename / default) |
|
|
149
|
+
| `gemiterm install-browser` | — | Install Playwright Chromium |
|
|
150
|
+
| `gemiterm install-skills` | — | Install the cross-agent skills bundle |
|
|
151
|
+
| `gemiterm -v <cmd>` | — | Verbose logging for any command |
|
|
152
|
+
|
|
153
|
+
### `gemiterm list`
|
|
154
|
+
|
|
155
|
+
List, filter, and search your Gemini chats.
|
|
143
156
|
|
|
144
157
|
```bash
|
|
145
|
-
gemiterm
|
|
158
|
+
gemiterm list
|
|
159
|
+
gemiterm list -n 50 --sort alpha
|
|
160
|
+
gemiterm list --search "react server components"
|
|
161
|
+
gemiterm list --after 2026-01-01 --before 2026-06-01
|
|
162
|
+
gemiterm list --all --all-profiles -f json -p chats.json
|
|
146
163
|
```
|
|
147
164
|
|
|
148
|
-
|
|
165
|
+
| Flag | Description |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `-n, --limit N` | Max chats to show (default: 10) |
|
|
168
|
+
| `--offset N` | Skip the first N chats |
|
|
169
|
+
| `--sort <recent\|oldest\|alpha>` | Sort order |
|
|
170
|
+
| `-s, --search <query>` | Filter by title |
|
|
171
|
+
| `--after <date>` | Only chats after this date |
|
|
172
|
+
| `--before <date>` | Only chats before this date |
|
|
173
|
+
| `--all` | Show all chats (no limit) |
|
|
174
|
+
| `--all-profiles` | Merge chats from all profiles |
|
|
175
|
+
| `-f, --format <text\|json>` | Output format |
|
|
176
|
+
| `-p, --path <path>` | Save output to file |
|
|
149
177
|
|
|
150
|
-
###
|
|
178
|
+
### `gemiterm fetch <conversation_id>`
|
|
179
|
+
|
|
180
|
+
Fetch a single conversation's full history.
|
|
151
181
|
|
|
152
182
|
```bash
|
|
153
|
-
gemiterm
|
|
183
|
+
gemiterm fetch c_abc123
|
|
184
|
+
gemiterm fetch c_abc123 -f json -p chat.json
|
|
154
185
|
```
|
|
155
186
|
|
|
156
|
-
|
|
157
|
-
|
|
187
|
+
| Flag | Description |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `-f, --format <text\|json>` | Output format |
|
|
190
|
+
| `-p, --path <path>` | Save output to file |
|
|
158
191
|
|
|
159
|
-
|
|
192
|
+
### `gemiterm new [message]`
|
|
160
193
|
|
|
161
|
-
|
|
194
|
+
Start a new chat. One-shot if you pass a message; otherwise drops into an interactive REPL.
|
|
162
195
|
|
|
163
196
|
```bash
|
|
164
|
-
gemiterm
|
|
197
|
+
gemiterm new "Explain the CAP theorem like I'm 12"
|
|
198
|
+
gemiterm new --profile work "Draft a status update"
|
|
199
|
+
gemiterm new --prompt-file ./long-prompt.md
|
|
165
200
|
```
|
|
166
201
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
202
|
+
| Flag | Description |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `-p, --profile <name>` | Use a specific profile |
|
|
205
|
+
| `-f, --prompt-file <path>` | Read the message from a file (bypasses shell arg-length limits) |
|
|
206
|
+
| `-h, --help` | Show help |
|
|
207
|
+
|
|
208
|
+
### `gemiterm continue <conversation_id> [message]`
|
|
171
209
|
|
|
172
|
-
|
|
210
|
+
Continue an existing chat. With no message, enters an interactive REPL. With no conversation ID, falls back to `list`.
|
|
173
211
|
|
|
174
212
|
```bash
|
|
175
|
-
gemiterm
|
|
213
|
+
gemiterm continue c_abc123
|
|
214
|
+
gemiterm continue c_abc123 "And what about edge cases?"
|
|
215
|
+
gemiterm continue c_abc123 --prompt-file ./refactor-context.md
|
|
176
216
|
```
|
|
177
217
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
-
|
|
181
|
-
|
|
182
|
-
- `-a, --all-profiles`: Export from all profiles
|
|
218
|
+
| Flag | Description |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `-f, --prompt-file <path>` | Read the message from a file (bypasses shell arg-length limits) |
|
|
221
|
+
| `-h, --help` | Show help |
|
|
183
222
|
|
|
184
|
-
|
|
223
|
+
> **Long prompts?** On Windows the shell argument limit is roughly 2 048 UTF-16 code units. GemiTerm detects oversized messages and **spills them to a temp file automatically** — or you can use `--prompt-file` explicitly to read from a file you control.
|
|
185
224
|
|
|
186
|
-
###
|
|
225
|
+
### `gemiterm export <conversation_id>`
|
|
226
|
+
|
|
227
|
+
Export a single chat to a file.
|
|
187
228
|
|
|
188
229
|
```bash
|
|
189
|
-
gemiterm
|
|
230
|
+
gemiterm export c_abc123
|
|
231
|
+
gemiterm export c_abc123 -o ./chat.md -f json --include-metadata
|
|
190
232
|
```
|
|
191
233
|
|
|
192
|
-
|
|
193
|
-
|
|
234
|
+
| Flag | Description |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| `-o, --output <path>` | Custom output file path |
|
|
237
|
+
| `-f, --format <markdown\|json>` | Export format (default: `markdown`) |
|
|
238
|
+
| `--include-metadata` | Include full metadata in the export |
|
|
239
|
+
|
|
240
|
+
### `gemiterm export-all`
|
|
194
241
|
|
|
195
|
-
|
|
242
|
+
Bulk-export every chat (or every chat since a date) with an auto-generated `index.md`.
|
|
196
243
|
|
|
197
244
|
```bash
|
|
198
|
-
gemiterm
|
|
199
|
-
gemiterm
|
|
200
|
-
gemiterm
|
|
201
|
-
gemiterm profile rename <name> <newName> # rename a profile
|
|
202
|
-
gemiterm profile default <name> # set default profile
|
|
245
|
+
gemiterm export-all
|
|
246
|
+
gemiterm export-all --since 2026-01-01 --include-metadata
|
|
247
|
+
gemiterm export-all -o ./exports -a
|
|
203
248
|
```
|
|
204
249
|
|
|
205
|
-
|
|
250
|
+
| Flag | Description |
|
|
251
|
+
| --- | --- |
|
|
252
|
+
| `-o, --output-dir <dir>` | Output directory (default: `./exports`) |
|
|
253
|
+
| `--since <date>` | Only chats newer than this date |
|
|
254
|
+
| `--include-metadata` | Include full metadata |
|
|
255
|
+
| `-a, --all-profiles` | Export from all profiles |
|
|
256
|
+
|
|
257
|
+
### `gemiterm delete <conversation_id>`
|
|
258
|
+
|
|
259
|
+
Delete a chat from your Gemini account. Confirms by default.
|
|
206
260
|
|
|
207
261
|
```bash
|
|
208
|
-
gemiterm
|
|
262
|
+
gemiterm delete c_abc123
|
|
263
|
+
gemiterm delete c_abc123 --force
|
|
209
264
|
```
|
|
210
265
|
|
|
211
|
-
|
|
266
|
+
| Flag | Description |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `-f, --force` | Skip the confirmation prompt |
|
|
269
|
+
|
|
270
|
+
### `gemiterm profile <subcommand>`
|
|
212
271
|
|
|
213
|
-
|
|
272
|
+
Manage multiple Gemini profiles (separate Google accounts or isolated cookie stores).
|
|
214
273
|
|
|
215
274
|
```bash
|
|
216
|
-
gemiterm
|
|
275
|
+
gemiterm profile list
|
|
276
|
+
gemiterm profile add work
|
|
277
|
+
gemiterm profile delete work
|
|
278
|
+
gemiterm profile rename work jobs
|
|
279
|
+
gemiterm profile default work
|
|
217
280
|
```
|
|
218
281
|
|
|
219
|
-
|
|
282
|
+
### `gemiterm install-browser`
|
|
283
|
+
|
|
284
|
+
Checks for a system Chrome / Edge first; falls back to Playwright's Chromium. Usually only needed once after install.
|
|
285
|
+
|
|
286
|
+
### `gemiterm install-skills`
|
|
220
287
|
|
|
221
|
-
|
|
288
|
+
Installs the [`opencode-gemiterm-skills`](https://www.npmjs.com/package/opencode-gemiterm-skills) bundle so your AI agent can use GemiTerm (and the `debate-with-gemini` skill) automatically.
|
|
222
289
|
|
|
223
290
|
## Configuration
|
|
224
291
|
|
|
225
|
-
### Configuration
|
|
292
|
+
### Configuration directory
|
|
226
293
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
294
|
+
| Platform | Config dir | Binary location (install script) |
|
|
295
|
+
| --- | --- | --- |
|
|
296
|
+
| Windows | `%APPDATA%\gemiterm\` | `%LOCALAPPDATA%\GemiTerm\` |
|
|
297
|
+
| Linux / macOS | `~/gemiterm/` | `~/.local/bin/gemiterm` |
|
|
230
298
|
|
|
231
299
|
Override with:
|
|
300
|
+
|
|
232
301
|
```bash
|
|
233
302
|
export GEMITERM_CONFIG_DIR=/custom/path
|
|
234
303
|
```
|
|
235
304
|
|
|
236
|
-
### Profile
|
|
305
|
+
### Profile storage
|
|
237
306
|
|
|
238
307
|
```
|
|
239
308
|
gemiterm/
|
|
240
309
|
profiles/
|
|
241
|
-
.default # text file with default profile name
|
|
310
|
+
.default # text file with the default profile name
|
|
242
311
|
<profile-name>/
|
|
243
|
-
storage_state.json #
|
|
312
|
+
storage_state.json # authenticated cookies
|
|
244
313
|
```
|
|
245
314
|
|
|
246
|
-
|
|
315
|
+
Upgrading from v1.4.1 keeps these paths and files unchanged.
|
|
316
|
+
|
|
317
|
+
### Environment variables
|
|
247
318
|
|
|
248
319
|
| Variable | Description | Default |
|
|
249
|
-
|
|
250
|
-
| `GEMITERM_CONFIG_DIR` |
|
|
320
|
+
| --- | --- | --- |
|
|
321
|
+
| `GEMITERM_CONFIG_DIR` | Override the configuration directory | Platform default |
|
|
251
322
|
|
|
252
|
-
##
|
|
323
|
+
## Contributing
|
|
253
324
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
fixtures/ # Shared test fixtures
|
|
266
|
-
```
|
|
325
|
+
Architecture, development workflow, build instructions, and contribution guidelines live in **[CONTRIBUTING.md](CONTRIBUTING.md)**. The agent guide — including the path-mediation rules, the sensitive auth files, and the OpenSpec workflow — lives in **[AGENTS.md](AGENTS.md)**.
|
|
326
|
+
|
|
327
|
+
## Acknowledgments
|
|
328
|
+
|
|
329
|
+
- [Google Gemini](https://gemini.google.com) — the model and web app this CLI wraps
|
|
330
|
+
- [Bun](https://bun.sh) — runtime, bundler, and the path to single-binary distribution
|
|
331
|
+
- [`@playwright/cli`](https://www.npmjs.com/package/@playwright/cli) — the headless browser that powers authentication
|
|
332
|
+
- [OpenCode](https://opencode.ai) and the broader AI-agent ecosystem — for the `debate-with-gemini` use case that inspired the skills bundle
|
|
333
|
+
- [`opencode-gemiterm-skills`](https://www.npmjs.com/package/opencode-gemiterm-skills) — the agent-skills companion package
|
|
334
|
+
|
|
335
|
+
## License
|
|
267
336
|
|
|
268
|
-
|
|
337
|
+
[MIT](LICENSE.md)
|
package/package.json
CHANGED
|
@@ -9,13 +9,18 @@ import {
|
|
|
9
9
|
type SendMessageCommandResult,
|
|
10
10
|
} from "../../core/command-handlers.ts";
|
|
11
11
|
import { runInteractiveLoop, type MessageHandlerResult } from "../utils/interactive-prompt.ts";
|
|
12
|
+
import { checkArgLength } from "../utils/long-arg-guard.ts";
|
|
13
|
+
import { loadPromptFromFile, spillOverToTempFile } from "../utils/prompt-file.ts";
|
|
14
|
+
import { removeFile } from "../../infrastructure/io.ts";
|
|
12
15
|
|
|
13
16
|
interface ContinueCommandOptions {
|
|
14
17
|
help: boolean;
|
|
18
|
+
promptFile: string | null;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const DEFAULT_OPTIONS: ContinueCommandOptions = {
|
|
18
22
|
help: false,
|
|
23
|
+
promptFile: null,
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
export class ContinueCommand implements CliCommand {
|
|
@@ -36,6 +41,7 @@ export class ContinueCommand implements CliCommand {
|
|
|
36
41
|
|
|
37
42
|
for (const arg of args) {
|
|
38
43
|
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
44
|
+
if (options.promptFile && arg === options.promptFile) continue;
|
|
39
45
|
if (!conversationId) {
|
|
40
46
|
conversationId = arg;
|
|
41
47
|
} else if (!message) {
|
|
@@ -43,11 +49,66 @@ export class ContinueCommand implements CliCommand {
|
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
if (options.promptFile) {
|
|
53
|
+
if (!conversationId) {
|
|
54
|
+
console.error(
|
|
55
|
+
chalk.red(
|
|
56
|
+
`Error: --prompt-file requires a conversation_id. ` +
|
|
57
|
+
`Specify a conversation to continue, e.g. \`gemiterm continue <conversation_id> --prompt-file <path>\`.`,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (message) {
|
|
63
|
+
console.error(
|
|
64
|
+
chalk.red(
|
|
65
|
+
`Error: cannot use --prompt-file together with a positional message argument. ` +
|
|
66
|
+
`Use one or the other, not both.`,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
if (!conversationId) {
|
|
47
74
|
await this.invokeListCommand(context);
|
|
48
75
|
return;
|
|
49
76
|
}
|
|
50
77
|
|
|
78
|
+
let effectivePromptFile: string | null = null;
|
|
79
|
+
let isSpillover = false;
|
|
80
|
+
if (options.promptFile) {
|
|
81
|
+
effectivePromptFile = options.promptFile;
|
|
82
|
+
} else if (message) {
|
|
83
|
+
const guard = checkArgLength(message);
|
|
84
|
+
if (!guard.safe) {
|
|
85
|
+
const spilled = await spillOverToTempFile(message);
|
|
86
|
+
effectivePromptFile = spilled;
|
|
87
|
+
isSpillover = true;
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.dim(
|
|
90
|
+
`[gemiterm] Message is ${guard.length} UTF-16 code units, exceeding the ${guard.limit} limit. ` +
|
|
91
|
+
`Spilled to temp file '${spilled}' and loading from there.`,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (effectivePromptFile) {
|
|
98
|
+
try {
|
|
99
|
+
message = await loadPromptFromFile(effectivePromptFile);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
if (isSpillover) {
|
|
105
|
+
try {
|
|
106
|
+
removeFile(effectivePromptFile);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
51
112
|
const mediator: Mediator = context.mediator;
|
|
52
113
|
|
|
53
114
|
if (message) {
|
|
@@ -128,9 +189,19 @@ export class ContinueCommand implements CliCommand {
|
|
|
128
189
|
private parseArgs(args: string[]): ContinueCommandOptions {
|
|
129
190
|
const options = { ...DEFAULT_OPTIONS };
|
|
130
191
|
|
|
131
|
-
for (
|
|
192
|
+
for (let i = 0; i < args.length; i++) {
|
|
193
|
+
const arg = args[i];
|
|
132
194
|
if (arg === "--help" || arg === "-h") {
|
|
133
195
|
options.help = true;
|
|
196
|
+
} else if (arg === "--prompt-file" || arg === "-f") {
|
|
197
|
+
const next = args[i + 1];
|
|
198
|
+
if (next && !next.startsWith("-")) {
|
|
199
|
+
options.promptFile = next;
|
|
200
|
+
i++;
|
|
201
|
+
} else {
|
|
202
|
+
console.error(chalk.red(`Error: --prompt-file requires a path`));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
134
205
|
}
|
|
135
206
|
}
|
|
136
207
|
|
|
@@ -149,6 +220,9 @@ export class ContinueCommand implements CliCommand {
|
|
|
149
220
|
);
|
|
150
221
|
console.log("");
|
|
151
222
|
console.log(chalk.bold("Options:"));
|
|
223
|
+
console.log(
|
|
224
|
+
` ${chalk.cyan("--prompt-file, -f <path>".padEnd(22))}${chalk.dim("Read the message from a file (bypasses the 2048 code unit arg limit)")}`,
|
|
225
|
+
);
|
|
152
226
|
console.log(` ${chalk.cyan("--help, -h".padEnd(22))}${chalk.dim("Show this help message")}`);
|
|
153
227
|
console.log("");
|
|
154
228
|
console.log(chalk.dim("If no conversation_id is provided, the list command will be invoked."));
|
|
@@ -8,15 +8,20 @@ import {
|
|
|
8
8
|
type StartNewChatCommandResult,
|
|
9
9
|
} from "../../core/command-handlers.ts";
|
|
10
10
|
import { runInteractiveLoop, type MessageHandlerResult } from "../utils/interactive-prompt.ts";
|
|
11
|
+
import { checkArgLength } from "../utils/long-arg-guard.ts";
|
|
12
|
+
import { loadPromptFromFile, spillOverToTempFile } from "../utils/prompt-file.ts";
|
|
13
|
+
import { removeFile } from "../../infrastructure/io.ts";
|
|
11
14
|
|
|
12
15
|
interface NewCommandOptions {
|
|
13
16
|
help: boolean;
|
|
14
17
|
profile: string | null;
|
|
18
|
+
promptFile: string | null;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const DEFAULT_OPTIONS: NewCommandOptions = {
|
|
18
22
|
help: false,
|
|
19
23
|
profile: null,
|
|
24
|
+
promptFile: null,
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
export class NewCommand implements CliCommand {
|
|
@@ -37,11 +42,56 @@ export class NewCommand implements CliCommand {
|
|
|
37
42
|
for (const arg of args) {
|
|
38
43
|
if (arg.startsWith("--") || arg.startsWith("-")) continue;
|
|
39
44
|
if (options.profile && arg === options.profile) continue;
|
|
45
|
+
if (options.promptFile && arg === options.promptFile) continue;
|
|
40
46
|
if (!message) {
|
|
41
47
|
message = arg;
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
if (options.promptFile && message) {
|
|
52
|
+
console.error(
|
|
53
|
+
chalk.red(
|
|
54
|
+
`Error: cannot use --prompt-file together with a positional message argument. ` +
|
|
55
|
+
`Use one or the other, not both.`,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let effectivePromptFile: string | null = null;
|
|
62
|
+
let isSpillover = false;
|
|
63
|
+
if (options.promptFile) {
|
|
64
|
+
effectivePromptFile = options.promptFile;
|
|
65
|
+
} else if (message) {
|
|
66
|
+
const guard = checkArgLength(message);
|
|
67
|
+
if (!guard.safe) {
|
|
68
|
+
const spilled = await spillOverToTempFile(message);
|
|
69
|
+
effectivePromptFile = spilled;
|
|
70
|
+
isSpillover = true;
|
|
71
|
+
console.log(
|
|
72
|
+
chalk.dim(
|
|
73
|
+
`[gemiterm] Message is ${guard.length} UTF-16 code units, exceeding the ${guard.limit} limit. ` +
|
|
74
|
+
`Spilled to temp file '${spilled}' and loading from there.`,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (effectivePromptFile) {
|
|
81
|
+
try {
|
|
82
|
+
message = await loadPromptFromFile(effectivePromptFile);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
if (isSpillover) {
|
|
88
|
+
try {
|
|
89
|
+
removeFile(effectivePromptFile);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
45
95
|
const mediator: Mediator = context.mediator;
|
|
46
96
|
|
|
47
97
|
if (message) {
|
|
@@ -121,6 +171,15 @@ export class NewCommand implements CliCommand {
|
|
|
121
171
|
console.error(chalk.red(`Error: --profile requires a profile name`));
|
|
122
172
|
process.exit(1);
|
|
123
173
|
}
|
|
174
|
+
} else if (arg === "--prompt-file" || arg === "-f") {
|
|
175
|
+
const next = args[i + 1];
|
|
176
|
+
if (next && !next.startsWith("-")) {
|
|
177
|
+
options.promptFile = next;
|
|
178
|
+
i++;
|
|
179
|
+
} else {
|
|
180
|
+
console.error(chalk.red(`Error: --prompt-file requires a path`));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
124
183
|
}
|
|
125
184
|
}
|
|
126
185
|
|
|
@@ -139,6 +198,9 @@ export class NewCommand implements CliCommand {
|
|
|
139
198
|
console.log(
|
|
140
199
|
` ${chalk.cyan("--profile, -p <name>".padEnd(22))}${chalk.dim("Use a specific profile (default profile used if omitted)")}`,
|
|
141
200
|
);
|
|
201
|
+
console.log(
|
|
202
|
+
` ${chalk.cyan("--prompt-file, -f <path>".padEnd(22))}${chalk.dim("Read the message from a file (bypasses the 2048 code unit arg limit)")}`,
|
|
203
|
+
);
|
|
142
204
|
console.log(` ${chalk.cyan("--help, -h".padEnd(22))}${chalk.dim("Show this help message")}`);
|
|
143
205
|
console.log("");
|
|
144
206
|
console.log(chalk.dim("If no message is provided, an interactive chat session will start."));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const WINDOWS_COMMAND_LINE_ARG_LIMIT = 2048;
|
|
2
|
+
|
|
3
|
+
export type LongArgGuardResult =
|
|
4
|
+
| { safe: true; arg: string }
|
|
5
|
+
| { safe: false; arg: string; length: number; limit: number; suggestion: string };
|
|
6
|
+
|
|
7
|
+
export function checkArgLength(arg: string): LongArgGuardResult {
|
|
8
|
+
const length = utf16CodeUnitLength(arg);
|
|
9
|
+
if (length <= WINDOWS_COMMAND_LINE_ARG_LIMIT) {
|
|
10
|
+
return { safe: true, arg };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
safe: false,
|
|
14
|
+
arg,
|
|
15
|
+
length,
|
|
16
|
+
limit: WINDOWS_COMMAND_LINE_ARG_LIMIT,
|
|
17
|
+
suggestion:
|
|
18
|
+
`Argument is ${length} UTF-16 code units, which exceeds the ${WINDOWS_COMMAND_LINE_ARG_LIMIT} ` +
|
|
19
|
+
`code unit limit imposed by Bun's Windows process spawn path (appendWindowsArgument in ` +
|
|
20
|
+
`Bun's run_command.zig panics with "index out of bounds" when this limit is exceeded). ` +
|
|
21
|
+
`Pipe the message via stdin (e.g. \`echo "..." | gemiterm new\`) or save it to a file and ` +
|
|
22
|
+
`pass it with \`--prompt-file <path>\`.`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function utf16CodeUnitLength(s: string): number {
|
|
27
|
+
return s.length;
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IOError, readTextFile, writeTextFile } from "../../infrastructure/io.ts";
|
|
2
|
+
import { getTempFilePath } from "../../infrastructure/path-utils.ts";
|
|
3
|
+
|
|
4
|
+
export async function loadPromptFromFile(filePath: string): Promise<string> {
|
|
5
|
+
try {
|
|
6
|
+
return readTextFile(filePath);
|
|
7
|
+
} catch (err) {
|
|
8
|
+
if (err instanceof IOError) {
|
|
9
|
+
throw new Error(`Could not read prompt file '${filePath}': ${err.message}`);
|
|
10
|
+
}
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function spillOverToTempFile(content: string): Promise<string> {
|
|
16
|
+
const path = getTempFilePath("gemiterm-arg-spill", ".txt");
|
|
17
|
+
try {
|
|
18
|
+
writeTextFile(path, content);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (err instanceof IOError) {
|
|
21
|
+
throw new Error(`Could not spill message to temp file: ${err.message}`);
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
return path;
|
|
26
|
+
}
|
package/src/infrastructure/io.ts
CHANGED
|
@@ -114,6 +114,14 @@ function removeDir(path: string): void {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function removeFile(path: string): void {
|
|
118
|
+
try {
|
|
119
|
+
rmSync(path, { force: true });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw wrap("removeFile", path, err instanceof Error ? err : undefined);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
117
125
|
function renameDir(src: string, dest: string): void {
|
|
118
126
|
try {
|
|
119
127
|
renameSync(src, dest);
|
|
@@ -155,6 +163,7 @@ export {
|
|
|
155
163
|
readJsonFile,
|
|
156
164
|
writeJsonFile,
|
|
157
165
|
removeDir,
|
|
166
|
+
removeFile,
|
|
158
167
|
renameDir,
|
|
159
168
|
isDirectory,
|
|
160
169
|
listSubdirectories,
|