companion-for-agy 1.2.0-alpha.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/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/README_de.md +126 -0
- package/ROADMAP.md +53 -0
- package/package.json +58 -0
- package/src/agy-companion.mjs +670 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.2.0-alpha.1] - 2026-06-07
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Package renamed to `companion-for-agy` (legal/trademark distancing via "for" pattern)
|
|
7
|
+
- Added "Unofficial" disclaimer to README and package description
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Short responses (≤2 chars like "4", "42", "ja") were incorrectly filtered as noise
|
|
11
|
+
- Prompt-echo bug in `--no-tools` mode: the permission prefix ("IMPORTANT: Do not use...") was returned as the response instead of the actual answer
|
|
12
|
+
- ConPTY space-loss in prompt echo: whitespace-normalized matching now handles "Donotuse" vs "Do not use"
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `stripPromptEcho()` — whitespace-tolerant prompt echo removal (word-by-word regex)
|
|
16
|
+
- `extractResponse()` now accepts 4th parameter `effectiveFilter` for full prompt echo stripping
|
|
17
|
+
- 5-phase state machine: Trust dialog auto-confirmation phase
|
|
18
|
+
- Banner model detection: JSON reports actual model from agy's banner
|
|
19
|
+
- 26 new tests (107 total): short answer extraction, prompt echo regression, stripPromptEcho unit tests
|
|
20
|
+
- `companion-for-agy` CLI alias (alongside `agy-companion` for backward compatibility)
|
|
21
|
+
|
|
22
|
+
## [1.1.0] - 2026-06-06
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Cross-platform support: Windows, macOS, Linux (node-pty handles platform-specific PTY)
|
|
26
|
+
- Auto-detection of agy binary via PATH, common install locations, env var fallback
|
|
27
|
+
- node-pty loaded as standard npm dependency (no hardcoded path to gemini-cli internals)
|
|
28
|
+
- Debug log writes to `./agy-debug.log` (CWD) instead of `~/.claude/scripts/`
|
|
29
|
+
- CLI messages and usage text in English for international audience
|
|
30
|
+
- Added English INIT_DONE_PATTERNS alongside German ones
|
|
31
|
+
- Removed `"os": ["win32"]` restriction from package.json
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- `findAgyPath()` exported function for programmatic agy detection
|
|
35
|
+
- Test suite: 81 tests (unit, fixture, smoke) via `node:test`
|
|
36
|
+
- `npm run deploy` and `npm run sync` scripts for local copy management
|
|
37
|
+
- Comprehensive README with installation, troubleshooting, and usage docs
|
|
38
|
+
|
|
39
|
+
## [1.0.0] - 2026-06-06
|
|
40
|
+
|
|
41
|
+
### Hinzugefügt
|
|
42
|
+
- ConPTY-basierter Wrapper für agy (Antigravity CLI)
|
|
43
|
+
- ANSI-Color-basierte Response-Extraktion (RGB 232,234,237)
|
|
44
|
+
- Fallback: Zeilen-basierte Noise-Filterung
|
|
45
|
+
- 4-Phasen State-Machine (Startup → Init → Question → Response)
|
|
46
|
+
- Adaptives Timing (10s während Generierung, 2.5s nach Abschluss)
|
|
47
|
+
- Permission-System mit 5 Modi: sandbox, skip-permissions, no-tools, researcher, read-only
|
|
48
|
+
- Custom allow/deny Regeln (kompatibel mit agys settings.json-Format)
|
|
49
|
+
- JSON-Output-Modus (--json)
|
|
50
|
+
- Konfigurierbare Pfade via Umgebungsvariablen
|
|
51
|
+
- Prompt-Sanitisierung gegen PTY-Injection
|
|
52
|
+
- Graceful Shutdown (Ctrl+C → Grace-Period → kill)
|
|
53
|
+
- Debug-Modus mit PTY-Output-Log
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lukas Geiger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# companion-for-agy
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/logo.png" alt="companion-for-agy Logo" width="200" height="200" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
[](README_de.md)
|
|
8
|
+
|
|
9
|
+
> **Unofficial** — not affiliated with or endorsed by Google.
|
|
10
|
+
|
|
11
|
+
PTY-based wrapper for **agy** (Antigravity CLI / Gemini CLI) that captures Gemini responses from subprocesses.
|
|
12
|
+
|
|
13
|
+
## Problem
|
|
14
|
+
|
|
15
|
+
`agy -p` (print mode) returns exit 0 but writes no output to stdout — the TUI text-drip renderer (`text_drip.go`) writes to the terminal buffer instead. This is a known bug:
|
|
16
|
+
|
|
17
|
+
- [antigravity-cli#76](https://github.com/google-antigravity/antigravity-cli/issues/76)
|
|
18
|
+
- [gemini-cli#27466](https://github.com/google-gemini/gemini-cli/issues/27466)
|
|
19
|
+
- [antigravity-cli#115](https://github.com/google-antigravity/antigravity-cli/issues/115)
|
|
20
|
+
|
|
21
|
+
This means no other agent (Claude Code, Codex, CI/CD) can programmatically read agy's responses.
|
|
22
|
+
|
|
23
|
+
## Solution
|
|
24
|
+
|
|
25
|
+
`companion-for-agy` spawns agy inside a virtual terminal via `node-pty` (ConPTY on Windows, forkpty on macOS/Linux) and extracts the response from the ANSI color stream. agy's response text uses `RGB(232,234,237)` — the wrapper tracks ANSI color state and collects only text in that color.
|
|
26
|
+
|
|
27
|
+
> **Platform note:** The ANSI color extraction (`RGB(232,234,237)`) and the `--model` flag have been verified on **Windows** with agy >= 1.1. macOS and Linux are expected to work via `node-pty` since agy uses the same Go TUI renderer. However:
|
|
28
|
+
> - **agy v1.0.x** (Homebrew `antigravity-cli`) does not support `--model` — the tool will fail with "flags provided but not defined". Update agy or omit `--model` (requires code change).
|
|
29
|
+
> - The exact RGB response color has not been independently confirmed on non-Windows platforms. If color extraction returns empty results, try `--debug` and check `agy-debug.log` for the actual color codes.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g companion-for-agy
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Prerequisites
|
|
38
|
+
|
|
39
|
+
- **Node.js >= 18**
|
|
40
|
+
- **agy** ([Gemini CLI](https://github.com/google-gemini/gemini-cli)) installed and authenticated
|
|
41
|
+
- **C/C++ build tools** for `node-pty` native compilation:
|
|
42
|
+
- **Windows:** [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + Python 3
|
|
43
|
+
- **macOS:** `xcode-select --install`
|
|
44
|
+
- **Linux:** `sudo apt install build-essential python3` (Debian/Ubuntu)
|
|
45
|
+
|
|
46
|
+
### Troubleshooting `node-pty` build errors
|
|
47
|
+
|
|
48
|
+
If `npm install` fails with native compilation errors:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# All platforms: rebuild native modules
|
|
52
|
+
npm rebuild node-pty
|
|
53
|
+
|
|
54
|
+
# Windows: if cl.exe is not found, install Visual Studio Build Tools
|
|
55
|
+
# then open "Developer Command Prompt" or run from "x64 Native Tools"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
companion-for-agy [options] "prompt"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Permission Modes
|
|
65
|
+
|
|
66
|
+
| Flag | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `--sandbox` | Sandbox mode (default) — tools in container |
|
|
69
|
+
| `--skip-permissions` | All tools without confirmation (YOLO) |
|
|
70
|
+
| `--no-tools` | Pure chat — no tool execution |
|
|
71
|
+
| `--researcher` | Web search allowed, no file changes |
|
|
72
|
+
| `--read-only` | Read-only, no modifications |
|
|
73
|
+
|
|
74
|
+
### Custom Rules
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
--allow "read_file(/path)" # Allowlist rule (repeatable)
|
|
78
|
+
--deny "command(rm)" # Denylist rule (repeatable)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Formats match agy's own permission system (`settings.json`).
|
|
82
|
+
|
|
83
|
+
### Options
|
|
84
|
+
|
|
85
|
+
| Flag | Description |
|
|
86
|
+
|------|-------------|
|
|
87
|
+
| `--model <model>` | Gemini model (default: `gemini-3.5-flash`) |
|
|
88
|
+
| `--timeout <ms>` | Timeout in ms (default: 120000) |
|
|
89
|
+
| `--json` | Output as JSON object |
|
|
90
|
+
| `--debug` | Save raw PTY output to `agy-debug.log` |
|
|
91
|
+
|
|
92
|
+
### Environment Variables
|
|
93
|
+
|
|
94
|
+
| Variable | Description |
|
|
95
|
+
|----------|-------------|
|
|
96
|
+
| `AGY_COMPANION_AGY_PATH` | Path to agy binary (auto-detected if not set) |
|
|
97
|
+
| `AGY_PATH` | Alternative path to agy binary |
|
|
98
|
+
|
|
99
|
+
### Examples
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Simple question
|
|
103
|
+
companion-for-agy "What is the capital of Bavaria?"
|
|
104
|
+
|
|
105
|
+
# As advisor (no tool use)
|
|
106
|
+
companion-for-agy --no-tools "Review this code: ..."
|
|
107
|
+
|
|
108
|
+
# Web research
|
|
109
|
+
companion-for-agy --researcher "Latest info on Node.js 24"
|
|
110
|
+
|
|
111
|
+
# Read-only with additional git permission
|
|
112
|
+
companion-for-agy --read-only --allow "command(git log)" "prompt"
|
|
113
|
+
|
|
114
|
+
# JSON output for programmatic use
|
|
115
|
+
companion-for-agy --json --model gemini-3.5-pro "prompt"
|
|
116
|
+
# → {"response":"...","model":"Gemini 3.5 Pro (High)","requestedModel":"gemini-3.5-pro","permissionMode":"sandbox"}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
> **JSON fields:** `model` reports the actual model detected from agy's banner (e.g., `"Gemini 3.5 Flash (Medium)"`). `requestedModel` is what was passed via `--model`. If banner detection fails, `model` falls back to `requestedModel`.
|
|
120
|
+
|
|
121
|
+
## How It Works
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
┌────────────────────┐ PTY ┌─────────────┐
|
|
125
|
+
│ companion-for-agy │ ─────────────▸ │ agy │
|
|
126
|
+
│ (Node.js) │ ◂────────────── │ (Go TUI) │
|
|
127
|
+
│ │ ANSI stream │ │
|
|
128
|
+
│ Color-Based │ │ text_drip.go│
|
|
129
|
+
│ Extraction │ │ RGB(232,234,│
|
|
130
|
+
│ │ │ 237) │
|
|
131
|
+
└────────┬───────────┘ └─────────────┘
|
|
132
|
+
│
|
|
133
|
+
▼ stdout
|
|
134
|
+
Response text
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**5-Phase State Machine:**
|
|
138
|
+
|
|
139
|
+
1. **Trust** — detect and auto-confirm workspace trust dialog
|
|
140
|
+
2. **Startup** — detect main UI readiness (`? for shortcuts`)
|
|
141
|
+
3. **Init** — wait for GEMINI.md initialization (pattern matching or 20s fallback)
|
|
142
|
+
4. **Question** — send prompt, set response marker
|
|
143
|
+
5. **Response** — read response via ANSI color extraction, adaptive idle timer
|
|
144
|
+
|
|
145
|
+
## Use Cases
|
|
146
|
+
|
|
147
|
+
- **Multi-agent orchestration**: Claude Code, Codex, or other agents querying Gemini via agy
|
|
148
|
+
- **CI/CD pipelines**: automated Gemini queries in build scripts
|
|
149
|
+
- **Scripting**: any scenario where agy's response needs to be captured as text
|
|
150
|
+
|
|
151
|
+
## Background
|
|
152
|
+
|
|
153
|
+
This tool was built because three CLI agents — **Claude Code**, **Codex**, and **agy** — need to call each other as fallback advisors. While Claude → Codex and agy → Claude/Codex already work, Claude → agy was blocked by the TUI stdout bug.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
package/README_de.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# companion-for-agy
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/logo.png" alt="companion-for-agy Logo" width="200" height="200" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
[](README.md)
|
|
8
|
+
|
|
9
|
+
> **Inoffiziell** — nicht verbunden mit oder empfohlen von Google.
|
|
10
|
+
|
|
11
|
+
PTY-basierter Wrapper für **agy** (Antigravity CLI / Gemini CLI), der Gemini-Antworten aus Subprozessen erfasst.
|
|
12
|
+
|
|
13
|
+
## Problem
|
|
14
|
+
|
|
15
|
+
`agy -p` (Print-Modus) gibt Exit 0 zurück, schreibt aber keinen Output nach stdout — der TUI Text-Drip-Renderer (`text_drip.go`) schreibt in den Terminal-Buffer. Das ist ein bekannter Bug:
|
|
16
|
+
|
|
17
|
+
- [antigravity-cli#76](https://github.com/google-antigravity/antigravity-cli/issues/76)
|
|
18
|
+
- [gemini-cli#27466](https://github.com/google-gemini/gemini-cli/issues/27466)
|
|
19
|
+
- [antigravity-cli#115](https://github.com/google-antigravity/antigravity-cli/issues/115)
|
|
20
|
+
|
|
21
|
+
Kein anderer Agent (Claude Code, Codex, CI/CD) kann dadurch agys Antworten programmatisch lesen.
|
|
22
|
+
|
|
23
|
+
## Lösung
|
|
24
|
+
|
|
25
|
+
`companion-for-agy` startet agy in einem virtuellen Terminal via `node-pty` (ConPTY unter Windows, forkpty unter macOS/Linux) und extrahiert die Antwort aus dem ANSI-Farbstream. agys Antworttext nutzt `RGB(232,234,237)` — der Wrapper verfolgt den ANSI-Farbstatus und sammelt nur Text in dieser Farbe.
|
|
26
|
+
|
|
27
|
+
> **Plattformhinweis:** Die ANSI-Farbextraktion (`RGB(232,234,237)`) wurde unter **Windows** (ConPTY) verifiziert. macOS und Linux sollten über `node-pty` funktionieren, da agy denselben Go-TUI-Renderer verwendet, aber die exakten RGB-Werte wurden auf diesen Plattformen noch nicht unabhängig bestätigt. Falls die Farbextraktion leere Ergebnisse liefert, `--debug` verwenden und `agy-debug.log` auf die tatsächlichen Farbcodes prüfen.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g companion-for-agy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Voraussetzungen
|
|
36
|
+
|
|
37
|
+
- **Node.js >= 18**
|
|
38
|
+
- **agy** ([Gemini CLI](https://github.com/google-gemini/gemini-cli)) installiert und authentifiziert
|
|
39
|
+
- **C/C++ Build-Tools** für `node-pty` Native-Kompilierung:
|
|
40
|
+
- **Windows:** [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + Python 3
|
|
41
|
+
- **macOS:** `xcode-select --install`
|
|
42
|
+
- **Linux:** `sudo apt install build-essential python3` (Debian/Ubuntu)
|
|
43
|
+
|
|
44
|
+
## Verwendung
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
companion-for-agy [optionen] "prompt"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Berechtigungsmodi
|
|
51
|
+
|
|
52
|
+
| Flag | Beschreibung |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `--sandbox` | Sandbox-Modus (Standard) — Tools im Container |
|
|
55
|
+
| `--skip-permissions` | Alle Tools ohne Bestätigung (YOLO) |
|
|
56
|
+
| `--no-tools` | Reiner Chat — keine Tool-Ausführung |
|
|
57
|
+
| `--researcher` | Web-Suche erlaubt, keine Dateiänderungen |
|
|
58
|
+
| `--read-only` | Nur Lesen, keine Änderungen |
|
|
59
|
+
|
|
60
|
+
### Eigene Regeln
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
--allow "read_file(/pfad)" # Erlaubnisregel (wiederholbar)
|
|
64
|
+
--deny "command(rm)" # Verbots-Regel (wiederholbar)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Optionen
|
|
68
|
+
|
|
69
|
+
| Flag | Beschreibung |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `--model <modell>` | Gemini-Modell (Standard: `gemini-3.5-flash`) |
|
|
72
|
+
| `--timeout <ms>` | Timeout in ms (Standard: 120000) |
|
|
73
|
+
| `--json` | Ausgabe als JSON-Objekt |
|
|
74
|
+
| `--debug` | Rohen PTY-Output in `agy-debug.log` speichern |
|
|
75
|
+
|
|
76
|
+
### Beispiele
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Einfache Frage
|
|
80
|
+
companion-for-agy "Was ist die Hauptstadt von Bayern?"
|
|
81
|
+
|
|
82
|
+
# Als Berater (keine Tool-Nutzung)
|
|
83
|
+
companion-for-agy --no-tools "Überprüfe diesen Code: ..."
|
|
84
|
+
|
|
85
|
+
# Web-Recherche
|
|
86
|
+
companion-for-agy --researcher "Neueste Infos zu Node.js 24"
|
|
87
|
+
|
|
88
|
+
# JSON-Output für programmatische Nutzung
|
|
89
|
+
companion-for-agy --json --model gemini-3.5-pro "prompt"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
> **JSON-Felder:** `model` meldet das tatsächliche Modell aus agys Banner (z.B. `"Gemini 3.5 Flash (Medium)"`). `requestedModel` ist was via `--model` übergeben wurde.
|
|
93
|
+
|
|
94
|
+
## Funktionsweise
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
┌────────────────────┐ PTY ┌─────────────┐
|
|
98
|
+
│ companion-for-agy │ ─────────────▸ │ agy │
|
|
99
|
+
│ (Node.js) │ ◂────────────── │ (Go TUI) │
|
|
100
|
+
│ │ ANSI-Stream │ │
|
|
101
|
+
│ Farbbasierte │ │ text_drip.go│
|
|
102
|
+
│ Extraktion │ │ RGB(232,234,│
|
|
103
|
+
│ │ │ 237) │
|
|
104
|
+
└────────┬───────────┘ └─────────────┘
|
|
105
|
+
│
|
|
106
|
+
▼ stdout
|
|
107
|
+
Antworttext
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**5-Phasen State Machine:**
|
|
111
|
+
|
|
112
|
+
1. **Trust** — Workspace-Trust-Dialog erkennen und automatisch bestätigen
|
|
113
|
+
2. **Startup** — Haupt-UI-Bereitschaft erkennen (`? for shortcuts`)
|
|
114
|
+
3. **Init** — GEMINI.md-Initialisierung abwarten (Pattern-Matching oder 20s Fallback)
|
|
115
|
+
4. **Question** — Prompt senden, Response-Marker setzen
|
|
116
|
+
5. **Response** — Antwort via ANSI-Farbextraktion lesen, adaptiver Idle-Timer
|
|
117
|
+
|
|
118
|
+
## Anwendungsfälle
|
|
119
|
+
|
|
120
|
+
- **Multi-Agent-Orchestrierung:** Claude Code, Codex oder andere Agenten fragen Gemini via agy
|
|
121
|
+
- **CI/CD-Pipelines:** Automatisierte Gemini-Abfragen in Build-Scripts
|
|
122
|
+
- **Scripting:** Jedes Szenario wo agys Antwort als Text benötigt wird
|
|
123
|
+
|
|
124
|
+
## Lizenz
|
|
125
|
+
|
|
126
|
+
MIT
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## Platform Status
|
|
4
|
+
|
|
5
|
+
| Platform | Status | Notes |
|
|
6
|
+
|----------|--------|-------|
|
|
7
|
+
| **Windows** | Verified | ConPTY, agy >= 1.1, RGB(232,234,237) confirmed |
|
|
8
|
+
| **macOS** | Untested | node-pty (forkpty) expected to work, color values unconfirmed |
|
|
9
|
+
| **Linux** | Untested | node-pty (forkpty) expected to work, color values unconfirmed |
|
|
10
|
+
|
|
11
|
+
## Planned
|
|
12
|
+
|
|
13
|
+
### macOS / Linux Support
|
|
14
|
+
|
|
15
|
+
The tool is currently **Windows-only verified**. macOS and Linux are expected to work via `node-pty` (which uses forkpty instead of ConPTY), but the following items need verification:
|
|
16
|
+
|
|
17
|
+
**TODOs:**
|
|
18
|
+
- [ ] Verify ANSI response color on macOS (is it still `RGB(232,234,237)` or does agy use a different palette?)
|
|
19
|
+
- [ ] Verify ANSI response color on Linux
|
|
20
|
+
- [ ] Handle agy v1.0.x (Homebrew `antigravity-cli`) which lacks `--model` flag — make `--model` conditional or skip it when agy version < 1.1
|
|
21
|
+
- [ ] Test node-pty spawn-helper permissions after `npm install` on macOS (prebuilt binaries need +x)
|
|
22
|
+
- [ ] Test trust dialog auto-confirmation flow on macOS/Linux
|
|
23
|
+
- [ ] Add platform-specific CI smoke tests (requires agy authentication in CI — may need to remain manual)
|
|
24
|
+
|
|
25
|
+
**Diagnostics available now:**
|
|
26
|
+
- `--debug` flag saves raw PTY output to `agy-debug.log` — inspect for actual ANSI color codes on any platform
|
|
27
|
+
- `AGY_COMPANION_RESPONSE_RGB` environment variable override (planned, not yet implemented)
|
|
28
|
+
|
|
29
|
+
### Color Fallback / Auto-Probe
|
|
30
|
+
The current ANSI color extraction relies on `RGB(232,234,237)` as the response color. This has been verified on Windows (ConPTY). If agy changes its color scheme or uses different values on macOS/Linux, extraction silently fails.
|
|
31
|
+
|
|
32
|
+
**Ideas:**
|
|
33
|
+
- `--probe-color`: Run a known-answer prompt ("What is 2+2?"), scan the raw ANSI stream for the color that wraps "4", and cache it per platform
|
|
34
|
+
- Platform-specific RGB override via environment variable (`AGY_COMPANION_RESPONSE_RGB`)
|
|
35
|
+
- Heuristic: find the most frequent non-UI color in the stream
|
|
36
|
+
|
|
37
|
+
### Multi-Turn Mode
|
|
38
|
+
Currently, each invocation spawns a fresh agy process (one question, one answer). A persistent mode that keeps the PTY alive across multiple prompts would reduce startup overhead for batch workloads.
|
|
39
|
+
|
|
40
|
+
### Streaming Output
|
|
41
|
+
Emit response tokens as they arrive (line-by-line or chunk-by-chunk) instead of buffering until completion. Useful for long responses where the caller wants progressive output.
|
|
42
|
+
|
|
43
|
+
### Response Format Detection
|
|
44
|
+
Detect whether agy's response is Markdown, JSON, or plain text and expose this in the JSON output (`"format": "markdown"`).
|
|
45
|
+
|
|
46
|
+
## Completed (v1.2.0-alpha.1)
|
|
47
|
+
|
|
48
|
+
- Trust dialog auto-confirmation (5-phase state machine)
|
|
49
|
+
- Banner model detection (actual model from agy's banner)
|
|
50
|
+
- Short response noise filter fix (answers like "4" or "42")
|
|
51
|
+
- Prompt-echo stripping in no-tools mode (ConPTY space-loss tolerant)
|
|
52
|
+
- Cross-platform agy binary auto-detection
|
|
53
|
+
- node-pty as standard npm dependency
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "companion-for-agy",
|
|
3
|
+
"version": "1.2.0-alpha.1",
|
|
4
|
+
"description": "Unofficial PTY-based wrapper for agy (Antigravity CLI / Gemini CLI) — captures Gemini responses that agy writes to the terminal buffer instead of stdout",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/agy-companion.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"companion-for-agy": "src/agy-companion.mjs",
|
|
9
|
+
"agy-companion": "src/agy-companion.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/agy-companion.mjs",
|
|
13
|
+
"README.md",
|
|
14
|
+
"README_de.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"ROADMAP.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"start": "node src/agy-companion.mjs",
|
|
21
|
+
"test": "node --test _tests/unit.test.mjs _tests/fixture.test.mjs",
|
|
22
|
+
"test:unit": "node --test _tests/unit.test.mjs",
|
|
23
|
+
"test:fixture": "node --test _tests/fixture.test.mjs",
|
|
24
|
+
"test:smoke": "node --test _tests/smoke.test.mjs",
|
|
25
|
+
"test:all": "node --test _tests/unit.test.mjs _tests/fixture.test.mjs _tests/smoke.test.mjs",
|
|
26
|
+
"deploy": "node -e \"require('fs').copyFileSync('src/agy-companion.mjs', require('path').join(require('os').homedir(), '.claude', 'scripts', 'agy-companion.mjs'));console.log('Deployed to ~/.claude/scripts/agy-companion.mjs')\"",
|
|
27
|
+
"sync": "git pull && npm run deploy"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"agy",
|
|
31
|
+
"antigravity",
|
|
32
|
+
"gemini",
|
|
33
|
+
"gemini-cli",
|
|
34
|
+
"cli",
|
|
35
|
+
"wrapper",
|
|
36
|
+
"pty",
|
|
37
|
+
"conpty",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"multi-agent",
|
|
40
|
+
"llm"
|
|
41
|
+
],
|
|
42
|
+
"author": "Lukas Geiger",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/dev-bricks/companion-for-agy.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/dev-bricks/companion-for-agy/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/dev-bricks/companion-for-agy#readme",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"node-pty": "^1.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* companion-for-agy — Unofficial PTY-based wrapper for agy (Antigravity CLI / Gemini CLI)
|
|
4
|
+
*
|
|
5
|
+
* Problem: agy's TUI text-drip renderer writes to the terminal buffer, not stdout.
|
|
6
|
+
* From subprocesses (Claude Code, Codex, CI/CD), no output is capturable.
|
|
7
|
+
* Additionally, GEMINI.md triggers a "first action" that consumes the
|
|
8
|
+
* single -p turn for session initialization.
|
|
9
|
+
*
|
|
10
|
+
* Solution: 1. node-pty creates a virtual terminal (ConPTY on Windows, forkpty on Unix)
|
|
11
|
+
* 2. Interactive mode: wait for init, THEN send the actual question
|
|
12
|
+
* 3. Extract response via ANSI color ([38;2;232;234;237m = response color)
|
|
13
|
+
* 4. Fallback: line-based noise filtering
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* agy-companion [options] "prompt"
|
|
17
|
+
* node src/agy-companion.mjs [options] "prompt"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import process from 'node:process';
|
|
27
|
+
|
|
28
|
+
// ---------- Defaults ----------
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_MODEL = 'gemini-3.5-flash';
|
|
31
|
+
export const DEFAULT_TIMEOUT_MS = 120000;
|
|
32
|
+
export const RESPONSE_IDLE_MS = 10000;
|
|
33
|
+
export const RESPONSE_DONE_IDLE_MS = 2500;
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
|
|
37
|
+
// ---------- Auto-Detection ----------
|
|
38
|
+
|
|
39
|
+
export function findAgyPath() {
|
|
40
|
+
if (process.env.AGY_COMPANION_AGY_PATH) return process.env.AGY_COMPANION_AGY_PATH;
|
|
41
|
+
if (process.env.AGY_PATH) return process.env.AGY_PATH;
|
|
42
|
+
|
|
43
|
+
const agyName = process.platform === 'win32' ? 'agy.exe' : 'agy';
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
47
|
+
const result = execFileSync(cmd, [agyName], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
50
|
+
}).trim();
|
|
51
|
+
const firstLine = result.split(/\r?\n/)[0].trim();
|
|
52
|
+
if (firstLine && fs.existsSync(firstLine)) return firstLine;
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
|
|
55
|
+
if (process.platform === 'win32') {
|
|
56
|
+
const localApp = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
57
|
+
const candidate = path.join(localApp, 'agy', 'bin', 'agy.exe');
|
|
58
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
59
|
+
} else {
|
|
60
|
+
for (const p of [
|
|
61
|
+
path.join(os.homedir(), '.local', 'bin', 'agy'),
|
|
62
|
+
'/usr/local/bin/agy',
|
|
63
|
+
'/opt/homebrew/bin/agy',
|
|
64
|
+
]) {
|
|
65
|
+
if (fs.existsSync(p)) return p;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const AGY_PATH = findAgyPath();
|
|
73
|
+
|
|
74
|
+
// ---------- Permission-Presets ----------
|
|
75
|
+
|
|
76
|
+
export const PERMISSION_PRESETS = {
|
|
77
|
+
sandbox: {
|
|
78
|
+
agyFlags: ['--sandbox'],
|
|
79
|
+
allow: [],
|
|
80
|
+
deny: [],
|
|
81
|
+
},
|
|
82
|
+
'skip-permissions': {
|
|
83
|
+
agyFlags: ['--dangerously-skip-permissions'],
|
|
84
|
+
allow: [],
|
|
85
|
+
deny: [],
|
|
86
|
+
},
|
|
87
|
+
'no-tools': {
|
|
88
|
+
agyFlags: ['--sandbox'],
|
|
89
|
+
allow: [],
|
|
90
|
+
deny: ['command(*)', 'write_file(*)', 'edit_file(*)', 'read_file(*)'],
|
|
91
|
+
promptPrefix: 'IMPORTANT: Do not use any tools. Answer based on your knowledge only.\n\n',
|
|
92
|
+
},
|
|
93
|
+
researcher: {
|
|
94
|
+
agyFlags: ['--sandbox'],
|
|
95
|
+
allow: ['google_search(*)', 'web_search(*)', 'web_fetch(*)', 'read_file(*)'],
|
|
96
|
+
deny: ['write_file(*)', 'edit_file(*)', 'command(rm *)', 'command(del *)'],
|
|
97
|
+
},
|
|
98
|
+
'read-only': {
|
|
99
|
+
agyFlags: ['--sandbox'],
|
|
100
|
+
allow: ['read_file(*)'],
|
|
101
|
+
deny: ['write_file(*)', 'edit_file(*)', 'command(rm *)', 'command(del *)'],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ---------- State-Machine-Patterns ----------
|
|
106
|
+
|
|
107
|
+
export const TRUST_DIALOG_PATTERN = /Do you trust/;
|
|
108
|
+
export const BANNER_MODEL_PATTERN = /Gemini \d[\d.]* \w+(?:\s*\([^)]*\))?/;
|
|
109
|
+
|
|
110
|
+
export const STARTUP_DONE_PATTERNS = [
|
|
111
|
+
/\? for shortcuts/,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export const INIT_DONE_PATTERNS = [
|
|
115
|
+
/Zusammenfassung der Arbeit/,
|
|
116
|
+
/Ich (bin|verwende|laufe) (derzeit |gerade )?(das Modell|auf dem Modell)/i,
|
|
117
|
+
/Das aktive Modell/i,
|
|
118
|
+
/Modell wurde als/i,
|
|
119
|
+
/aktive Modell/i,
|
|
120
|
+
/active model/i,
|
|
121
|
+
/using model/i,
|
|
122
|
+
/session (initialized|started|ready)/i,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
export const INIT_FALLBACK_MS = 20000;
|
|
126
|
+
|
|
127
|
+
// ---------- Prompt-Sanitisierung ----------
|
|
128
|
+
|
|
129
|
+
export function sanitizeForPty(text) {
|
|
130
|
+
return text
|
|
131
|
+
.replace(/[\x00-\x08\x0b\x0e-\x1f]/g, '')
|
|
132
|
+
.replace(/\x03/g, '')
|
|
133
|
+
.replace(/\r\n|\r|\n/g, ' ');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------- ANSI/VT100 Bereinigung ----------
|
|
137
|
+
|
|
138
|
+
export function stripAnsi(raw) {
|
|
139
|
+
return raw
|
|
140
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
|
|
141
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
142
|
+
.replace(/\x1b[()][AB012]/g, '')
|
|
143
|
+
.replace(/\x1b[=><NOMlmiHI78DEHMNO]/g, '')
|
|
144
|
+
.replace(/\x1b./g, '')
|
|
145
|
+
.replace(/[\x00-\x08\x0b\x0e-\x1f\x7f]/g, '')
|
|
146
|
+
.replace(/\r\n/g, '\n')
|
|
147
|
+
.replace(/\r/g, '\n');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function isNoiseLine(line, promptFilter = '') {
|
|
151
|
+
const t = line.trim();
|
|
152
|
+
if (!t) return true;
|
|
153
|
+
if (/^[│┌└┐┘├┤┬┴┼─═╔╗╚╝╠╣╦╩╬▸►◉●▲▼◆□■╭╮╯╰]+$/.test(t)) return true;
|
|
154
|
+
if (t.startsWith('>')) return true;
|
|
155
|
+
if (/[⣾⣷⣯⣟⡿⢿⣻⣽⠿⠾⠽⠼⠻⠺⠹⠸⠷⠶⠵⠴⠳⠲⠱⠰]/.test(t)) return true;
|
|
156
|
+
if (/Generating|esc to cancel|for shortcuts|tokens/i.test(t)) return true;
|
|
157
|
+
if (t === '?' || t === '? for shortcuts') return true;
|
|
158
|
+
if (/^Gemini \d/.test(t)) return true;
|
|
159
|
+
if (/^\d+\s*tokens$/.test(t)) return true;
|
|
160
|
+
if (/^▸\s/.test(t)) return true;
|
|
161
|
+
if (/^(Checking|Reading|Writing|Searching|Fetching|Analyzing|Executing)\b/i.test(t)) return true;
|
|
162
|
+
if (t.includes('@googlemail.com') || t.includes('@gmail.com')) return true;
|
|
163
|
+
if (promptFilter && t.includes(promptFilter.slice(0, 20))) return true;
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------- Response-Extraktion via ANSI-Farbe ----------
|
|
168
|
+
|
|
169
|
+
export function extractByResponseColor(rawSection) {
|
|
170
|
+
const segments = [];
|
|
171
|
+
let inResponseColor = false;
|
|
172
|
+
let pos = 0;
|
|
173
|
+
const src = rawSection;
|
|
174
|
+
|
|
175
|
+
while (pos < src.length) {
|
|
176
|
+
if (src[pos] === '\x1b') {
|
|
177
|
+
if (src[pos + 1] === '[') {
|
|
178
|
+
let end = pos + 2;
|
|
179
|
+
while (end < src.length && !/[A-Za-z]/.test(src[end])) end++;
|
|
180
|
+
const cmd = src[end];
|
|
181
|
+
const params = src.slice(pos + 2, end);
|
|
182
|
+
if (params === '38;2;232;234;237' && cmd === 'm') {
|
|
183
|
+
inResponseColor = true;
|
|
184
|
+
} else if (cmd === 'm') {
|
|
185
|
+
inResponseColor = false;
|
|
186
|
+
}
|
|
187
|
+
pos = end + 1;
|
|
188
|
+
} else if (src[pos + 1] === ']') {
|
|
189
|
+
let end = pos + 2;
|
|
190
|
+
while (end < src.length && src[end] !== '\x07' &&
|
|
191
|
+
!(src[end] === '\x1b' && src[end + 1] === '\\')) end++;
|
|
192
|
+
pos = src[end] === '\x07' ? end + 1 : end + 2;
|
|
193
|
+
} else {
|
|
194
|
+
pos += 2;
|
|
195
|
+
}
|
|
196
|
+
} else if (inResponseColor) {
|
|
197
|
+
let textEnd = pos;
|
|
198
|
+
while (textEnd < src.length && src[textEnd] !== '\x1b') textEnd++;
|
|
199
|
+
const rawText = src.slice(pos, textEnd);
|
|
200
|
+
const text = rawText.replace(/[\x00-\x08\x0b\x0e-\x1f\x7f]/g, '');
|
|
201
|
+
if (text.length > 0) segments.push(text);
|
|
202
|
+
pos = textEnd;
|
|
203
|
+
} else {
|
|
204
|
+
pos++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (segments.length === 0) return null;
|
|
209
|
+
|
|
210
|
+
const deduped = segments.filter((s, i) =>
|
|
211
|
+
!segments.some((o, j) => j !== i && o.length > s.length && o.startsWith(s))
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const combined = deduped.join('').trim();
|
|
215
|
+
return combined.length > 0 ? combined : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function escapeRegex(s) {
|
|
219
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function stripPromptEcho(text, filter) {
|
|
223
|
+
if (!filter || !text) return null;
|
|
224
|
+
const normText = text.replace(/\s+/g, '');
|
|
225
|
+
const normFilter = filter.replace(/\s+/g, '');
|
|
226
|
+
const matchLen = Math.min(30, normFilter.length);
|
|
227
|
+
if (matchLen < 5 || !normText.includes(normFilter.slice(0, matchLen))) return null;
|
|
228
|
+
const words = filter.split(/\s+/).filter(w => w.length > 0);
|
|
229
|
+
const regexStr = words.map(w => escapeRegex(w)).join('\\s*');
|
|
230
|
+
const clean = text.replace(new RegExp(regexStr), '').trim();
|
|
231
|
+
return clean.length > 0 ? clean : '';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function extractResponse(stripped, rawSection, promptFilter = '', effectiveFilter = '') {
|
|
235
|
+
if (rawSection) {
|
|
236
|
+
const colorResult = extractByResponseColor(rawSection);
|
|
237
|
+
if (colorResult && colorResult.length > 0) {
|
|
238
|
+
const echoFilter = effectiveFilter || promptFilter;
|
|
239
|
+
if (echoFilter) {
|
|
240
|
+
const cleaned = stripPromptEcho(colorResult, echoFilter);
|
|
241
|
+
if (cleaned !== null) return cleaned || null;
|
|
242
|
+
}
|
|
243
|
+
if (promptFilter && promptFilter !== echoFilter) {
|
|
244
|
+
const cleaned = stripPromptEcho(colorResult, promptFilter);
|
|
245
|
+
if (cleaned !== null) return cleaned || null;
|
|
246
|
+
}
|
|
247
|
+
return colorResult;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const lines = stripped.split('\n');
|
|
252
|
+
const meaningful = lines.filter(l => !isNoiseLine(l, promptFilter));
|
|
253
|
+
if (meaningful.length === 0) return null;
|
|
254
|
+
|
|
255
|
+
let best = meaningful.join('\n').trim();
|
|
256
|
+
if (promptFilter) {
|
|
257
|
+
const promptWords = promptFilter.split(/\s+/).slice(0, 5).join('');
|
|
258
|
+
best = best.split('\n')
|
|
259
|
+
.filter(l => !l.replace(/\s/g, '').startsWith(promptWords.replace(/\s/g, '')))
|
|
260
|
+
.join('\n')
|
|
261
|
+
.trim();
|
|
262
|
+
}
|
|
263
|
+
return best || null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------- CLI Main ----------
|
|
267
|
+
|
|
268
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
269
|
+
|
|
270
|
+
function isMainModule() {
|
|
271
|
+
try {
|
|
272
|
+
if (!process.argv[1]) return false;
|
|
273
|
+
return fs.realpathSync(path.resolve(process.argv[1])) === fs.realpathSync(path.resolve(__filename));
|
|
274
|
+
} catch (_) {
|
|
275
|
+
return path.resolve(process.argv[1] || '') === path.resolve(__filename);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isMainModule()) {
|
|
280
|
+
|
|
281
|
+
function printUsage() {
|
|
282
|
+
process.stderr.write([
|
|
283
|
+
'companion-for-agy — Unofficial PTY wrapper for agy (Antigravity CLI / Gemini CLI)',
|
|
284
|
+
'',
|
|
285
|
+
'Usage: companion-for-agy [options] "prompt"',
|
|
286
|
+
'',
|
|
287
|
+
'Permission modes (mutually exclusive):',
|
|
288
|
+
' --sandbox Sandbox mode (default) — tools in container',
|
|
289
|
+
' --skip-permissions All tools without confirmation (YOLO)',
|
|
290
|
+
' --no-tools Pure chat — no tool execution',
|
|
291
|
+
' --researcher Web search allowed, no file changes',
|
|
292
|
+
' --read-only Read-only, no modifications',
|
|
293
|
+
'',
|
|
294
|
+
'Custom rules (combinable with modes):',
|
|
295
|
+
' --allow <pattern> Allowlist rule (repeatable)',
|
|
296
|
+
' --deny <pattern> Denylist rule (repeatable)',
|
|
297
|
+
' Formats: read_file(/path), command(git), write_file(*)',
|
|
298
|
+
'',
|
|
299
|
+
'Options:',
|
|
300
|
+
' --model <model> Gemini model (default: gemini-3.5-flash)',
|
|
301
|
+
' --timeout <ms> Timeout in ms (default: 120000)',
|
|
302
|
+
' --json Output as JSON object',
|
|
303
|
+
' --debug Save raw PTY output to agy-debug.log',
|
|
304
|
+
' --help Show this help',
|
|
305
|
+
'',
|
|
306
|
+
'Models:',
|
|
307
|
+
' gemini-1.5-flash gemini-1.5-pro',
|
|
308
|
+
' gemini-2.0-flash gemini-2.0-pro',
|
|
309
|
+
' gemini-3.5-flash gemini-3.5-pro',
|
|
310
|
+
'',
|
|
311
|
+
'Environment variables:',
|
|
312
|
+
' AGY_COMPANION_AGY_PATH Path to agy binary',
|
|
313
|
+
' AGY_PATH Alternative path to agy binary',
|
|
314
|
+
'',
|
|
315
|
+
'Examples:',
|
|
316
|
+
' companion-for-agy "What is the capital of Bavaria?"',
|
|
317
|
+
' companion-for-agy --no-tools "Review this code: ..."',
|
|
318
|
+
' companion-for-agy --researcher "Latest info on Node.js 24"',
|
|
319
|
+
' companion-for-agy --read-only --allow "command(git log)" "prompt"',
|
|
320
|
+
' companion-for-agy --json --model gemini-3.5-pro "prompt"',
|
|
321
|
+
].join('\n') + '\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const rawArgs = process.argv.slice(2);
|
|
325
|
+
if (rawArgs.length === 0 || rawArgs[0] === '--help' || rawArgs[0] === '-h') {
|
|
326
|
+
printUsage();
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let model = DEFAULT_MODEL;
|
|
331
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
332
|
+
let debug = false;
|
|
333
|
+
let jsonOutput = false;
|
|
334
|
+
let permissionMode = 'sandbox';
|
|
335
|
+
const customAllow = [];
|
|
336
|
+
const customDeny = [];
|
|
337
|
+
let userPromptForFilter = '';
|
|
338
|
+
let effectivePromptForFilter = '';
|
|
339
|
+
const promptParts = [];
|
|
340
|
+
|
|
341
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
342
|
+
const arg = rawArgs[i];
|
|
343
|
+
if ((arg === '--model' || arg === '-m') && rawArgs[i + 1]) {
|
|
344
|
+
model = rawArgs[++i];
|
|
345
|
+
} else if (arg === '--timeout' && rawArgs[i + 1]) {
|
|
346
|
+
const t = parseInt(rawArgs[++i], 10);
|
|
347
|
+
if (!isNaN(t) && t > 0) timeoutMs = t;
|
|
348
|
+
} else if (arg === '--debug') {
|
|
349
|
+
debug = true;
|
|
350
|
+
} else if (arg === '--json') {
|
|
351
|
+
jsonOutput = true;
|
|
352
|
+
} else if (arg === '--sandbox') {
|
|
353
|
+
permissionMode = 'sandbox';
|
|
354
|
+
} else if (arg === '--skip-permissions') {
|
|
355
|
+
permissionMode = 'skip-permissions';
|
|
356
|
+
} else if (arg === '--no-tools') {
|
|
357
|
+
permissionMode = 'no-tools';
|
|
358
|
+
} else if (arg === '--researcher') {
|
|
359
|
+
permissionMode = 'researcher';
|
|
360
|
+
} else if (arg === '--read-only') {
|
|
361
|
+
permissionMode = 'read-only';
|
|
362
|
+
} else if (arg === '--allow' && rawArgs[i + 1]) {
|
|
363
|
+
customAllow.push(rawArgs[++i]);
|
|
364
|
+
} else if (arg === '--deny' && rawArgs[i + 1]) {
|
|
365
|
+
customDeny.push(rawArgs[++i]);
|
|
366
|
+
} else if (!arg.startsWith('-')) {
|
|
367
|
+
promptParts.push(arg);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const userPrompt = promptParts.join(' ').trim();
|
|
372
|
+
if (!userPrompt) {
|
|
373
|
+
process.stderr.write('Error: No prompt provided.\n\n');
|
|
374
|
+
printUsage();
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
userPromptForFilter = userPrompt;
|
|
378
|
+
|
|
379
|
+
// ---------- Permission-Setup ----------
|
|
380
|
+
|
|
381
|
+
const preset = PERMISSION_PRESETS[permissionMode];
|
|
382
|
+
const allAllow = [...preset.allow, ...customAllow];
|
|
383
|
+
const allDeny = [...preset.deny, ...customDeny];
|
|
384
|
+
|
|
385
|
+
const tempWorkspace = path.join(os.tmpdir(), `agy-companion-${process.pid}`);
|
|
386
|
+
let tempSettingsCreated = false;
|
|
387
|
+
|
|
388
|
+
if (allAllow.length > 0 || allDeny.length > 0) {
|
|
389
|
+
const geminiDir = path.join(tempWorkspace, '.gemini');
|
|
390
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
391
|
+
const settings = { permissions: {} };
|
|
392
|
+
if (allAllow.length > 0) settings.permissions.allow = allAllow;
|
|
393
|
+
if (allDeny.length > 0) settings.permissions.deny = allDeny;
|
|
394
|
+
fs.writeFileSync(path.join(geminiDir, 'settings.json'), JSON.stringify(settings, null, 2));
|
|
395
|
+
tempSettingsCreated = true;
|
|
396
|
+
} else {
|
|
397
|
+
fs.mkdirSync(tempWorkspace, { recursive: true });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const promptPrefix = preset.promptPrefix || '';
|
|
401
|
+
const effectivePrompt = promptPrefix + userPrompt;
|
|
402
|
+
effectivePromptForFilter = effectivePrompt;
|
|
403
|
+
|
|
404
|
+
// ---------- node-pty ----------
|
|
405
|
+
|
|
406
|
+
let pty;
|
|
407
|
+
const nodePtyOverride = process.env.AGY_COMPANION_PTY_PATH;
|
|
408
|
+
|
|
409
|
+
if (nodePtyOverride) {
|
|
410
|
+
try {
|
|
411
|
+
pty = require(nodePtyOverride);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
process.stderr.write(`[agy-companion] Failed to load node-pty from ${nodePtyOverride}:\n ${err.message}\n`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
try {
|
|
418
|
+
pty = require('node-pty');
|
|
419
|
+
} catch (_) {
|
|
420
|
+
let loaded = false;
|
|
421
|
+
const geminiPtySuffix = path.join('@google', 'gemini-cli', 'node_modules', 'node-pty');
|
|
422
|
+
const candidates = [];
|
|
423
|
+
|
|
424
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
425
|
+
candidates.push(path.join(process.env.APPDATA, 'npm', 'node_modules', geminiPtySuffix));
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const globalRoot = execSync('npm root -g', {
|
|
429
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
430
|
+
}).trim();
|
|
431
|
+
candidates.push(path.join(globalRoot, geminiPtySuffix));
|
|
432
|
+
} catch (_) { /* npm not available */ }
|
|
433
|
+
|
|
434
|
+
for (const candidate of candidates) {
|
|
435
|
+
try {
|
|
436
|
+
pty = require(candidate);
|
|
437
|
+
loaded = true;
|
|
438
|
+
break;
|
|
439
|
+
} catch (_) { /* try next */ }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!loaded) {
|
|
443
|
+
process.stderr.write(
|
|
444
|
+
`[agy-companion] Failed to load node-pty.\n` +
|
|
445
|
+
`[agy-companion] Install via: npm install -g companion-for-agy\n` +
|
|
446
|
+
`[agy-companion] Or set AGY_COMPANION_PTY_PATH environment variable.\n`
|
|
447
|
+
);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------- Resolve agy path ----------
|
|
454
|
+
|
|
455
|
+
const resolvedAgyPath = AGY_PATH;
|
|
456
|
+
if (!resolvedAgyPath) {
|
|
457
|
+
process.stderr.write(
|
|
458
|
+
`[agy-companion] agy not found.\n` +
|
|
459
|
+
`[agy-companion] Install agy: https://github.com/google-gemini/gemini-cli\n` +
|
|
460
|
+
`[agy-companion] Or set AGY_PATH / AGY_COMPANION_AGY_PATH environment variable.\n`
|
|
461
|
+
);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!fs.existsSync(resolvedAgyPath)) {
|
|
466
|
+
process.stderr.write(`[agy-companion] agy not found at: ${resolvedAgyPath}\n`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------- Start agy ----------
|
|
471
|
+
|
|
472
|
+
const agyArgs = ['--model', model, ...preset.agyFlags];
|
|
473
|
+
|
|
474
|
+
process.stderr.write(
|
|
475
|
+
`[agy-companion] Starting agy ${agyArgs.join(' ')} (${permissionMode})...\n`
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const ptyProc = pty.spawn(resolvedAgyPath, agyArgs, {
|
|
479
|
+
name: 'xterm-256color',
|
|
480
|
+
cols: 220,
|
|
481
|
+
rows: 50,
|
|
482
|
+
cwd: tempWorkspace,
|
|
483
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
let rawBuffer = '';
|
|
487
|
+
let detectedModel = null;
|
|
488
|
+
let trustHandled = false;
|
|
489
|
+
let startupComplete = false;
|
|
490
|
+
let initDone = false;
|
|
491
|
+
let questionSent = false;
|
|
492
|
+
let responseStartMark = 0;
|
|
493
|
+
let initIdleTimer = null;
|
|
494
|
+
let responseIdleTimer = null;
|
|
495
|
+
let finished = false;
|
|
496
|
+
|
|
497
|
+
const globalTimeout = setTimeout(() => {
|
|
498
|
+
if (!finished) {
|
|
499
|
+
process.stderr.write(`[agy-companion] Global timeout (${timeoutMs}ms). Aborting.\n`);
|
|
500
|
+
shutdown(2);
|
|
501
|
+
}
|
|
502
|
+
}, timeoutMs);
|
|
503
|
+
|
|
504
|
+
function cleanupTemp() {
|
|
505
|
+
try {
|
|
506
|
+
if (tempSettingsCreated) {
|
|
507
|
+
fs.rmSync(tempWorkspace, { recursive: true, force: true });
|
|
508
|
+
} else {
|
|
509
|
+
fs.rmdirSync(tempWorkspace);
|
|
510
|
+
}
|
|
511
|
+
} catch (_) {}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function shutdown(code) {
|
|
515
|
+
if (finished) return;
|
|
516
|
+
finished = true;
|
|
517
|
+
clearTimeout(globalTimeout);
|
|
518
|
+
clearTimeout(initIdleTimer);
|
|
519
|
+
clearTimeout(responseIdleTimer);
|
|
520
|
+
|
|
521
|
+
try { ptyProc.write('\x03'); } catch (_) {}
|
|
522
|
+
setTimeout(() => {
|
|
523
|
+
try { ptyProc.kill(); } catch (_) {}
|
|
524
|
+
|
|
525
|
+
if (debug) {
|
|
526
|
+
const debugPath = path.resolve('agy-debug.log');
|
|
527
|
+
try { fs.writeFileSync(debugPath, rawBuffer, 'utf8'); } catch (_) {}
|
|
528
|
+
process.stderr.write(`[agy-companion] Debug log: ${debugPath}\n`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
cleanupTemp();
|
|
532
|
+
process.exit(code);
|
|
533
|
+
}, 500);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function deliverResponse() {
|
|
537
|
+
const responsePart = rawBuffer.slice(responseStartMark);
|
|
538
|
+
const stripped = stripAnsi(responsePart);
|
|
539
|
+
const response = extractResponse(stripped, responsePart, userPromptForFilter, effectivePromptForFilter);
|
|
540
|
+
|
|
541
|
+
if (response) {
|
|
542
|
+
outputResult(response);
|
|
543
|
+
shutdown(0);
|
|
544
|
+
} else {
|
|
545
|
+
const fullStripped = stripAnsi(rawBuffer);
|
|
546
|
+
const fallback = extractResponse(fullStripped, rawBuffer, userPromptForFilter, effectivePromptForFilter);
|
|
547
|
+
if (fallback) {
|
|
548
|
+
outputResult(fallback);
|
|
549
|
+
} else {
|
|
550
|
+
process.stderr.write('[agy-companion] No usable response received.\n');
|
|
551
|
+
}
|
|
552
|
+
shutdown(0);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function outputResult(text) {
|
|
557
|
+
if (jsonOutput) {
|
|
558
|
+
const result = { response: text, model: detectedModel || model, requestedModel: model, permissionMode };
|
|
559
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
560
|
+
} else {
|
|
561
|
+
process.stdout.write(text + '\n');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function sendQuestion() {
|
|
566
|
+
if (questionSent) return;
|
|
567
|
+
questionSent = true;
|
|
568
|
+
responseStartMark = rawBuffer.length;
|
|
569
|
+
process.stderr.write(`[agy-companion] Init complete. Sending question...\n`);
|
|
570
|
+
ptyProc.write(sanitizeForPty(effectivePrompt) + '\r');
|
|
571
|
+
|
|
572
|
+
responseIdleTimer = setTimeout(() => {
|
|
573
|
+
process.stderr.write(`[agy-companion] Response idle timeout. Extracting...\n`);
|
|
574
|
+
deliverResponse();
|
|
575
|
+
}, RESPONSE_IDLE_MS);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
ptyProc.onData(chunk => {
|
|
579
|
+
rawBuffer += chunk;
|
|
580
|
+
const recentStripped = stripAnsi(rawBuffer.slice(-3000));
|
|
581
|
+
|
|
582
|
+
if (!questionSent && !finished) {
|
|
583
|
+
if (!detectedModel) {
|
|
584
|
+
const modelMatch = recentStripped.match(BANNER_MODEL_PATTERN);
|
|
585
|
+
if (modelMatch) {
|
|
586
|
+
detectedModel = modelMatch[0];
|
|
587
|
+
process.stderr.write(`[agy-companion] Detected model: ${detectedModel}\n`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (!trustHandled && TRUST_DIALOG_PATTERN.test(recentStripped)) {
|
|
592
|
+
trustHandled = true;
|
|
593
|
+
process.stderr.write(`[agy-companion] Trust dialog detected. Auto-confirming...\n`);
|
|
594
|
+
ptyProc.write('\r');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!startupComplete) {
|
|
598
|
+
if (STARTUP_DONE_PATTERNS.some(p => p.test(recentStripped))) {
|
|
599
|
+
startupComplete = true;
|
|
600
|
+
process.stderr.write(`[agy-companion] Startup complete. Waiting for init...\n`);
|
|
601
|
+
clearTimeout(initIdleTimer);
|
|
602
|
+
initIdleTimer = setTimeout(() => {
|
|
603
|
+
if (!initDone && !questionSent) {
|
|
604
|
+
initDone = true;
|
|
605
|
+
process.stderr.write(`[agy-companion] Init fallback timeout (${INIT_FALLBACK_MS}ms).\n`);
|
|
606
|
+
sendQuestion();
|
|
607
|
+
}
|
|
608
|
+
}, INIT_FALLBACK_MS);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (startupComplete && !initDone) {
|
|
613
|
+
if (INIT_DONE_PATTERNS.some(p => p.test(recentStripped))) {
|
|
614
|
+
initDone = true;
|
|
615
|
+
clearTimeout(initIdleTimer);
|
|
616
|
+
process.stderr.write(`[agy-companion] Init detected. Brief pause...\n`);
|
|
617
|
+
setTimeout(sendQuestion, 1000);
|
|
618
|
+
} else {
|
|
619
|
+
clearTimeout(initIdleTimer);
|
|
620
|
+
initIdleTimer = setTimeout(() => {
|
|
621
|
+
if (!initDone && !questionSent) {
|
|
622
|
+
initDone = true;
|
|
623
|
+
process.stderr.write(`[agy-companion] Init idle timeout.\n`);
|
|
624
|
+
sendQuestion();
|
|
625
|
+
}
|
|
626
|
+
}, INIT_FALLBACK_MS);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} else if (questionSent && !finished) {
|
|
630
|
+
clearTimeout(responseIdleTimer);
|
|
631
|
+
|
|
632
|
+
const responseSoFar = stripAnsi(rawBuffer.slice(responseStartMark));
|
|
633
|
+
const respLines = responseSoFar.split('\n');
|
|
634
|
+
let seenQuestionEcho = false;
|
|
635
|
+
let responseComplete = false;
|
|
636
|
+
for (const line of respLines) {
|
|
637
|
+
const t = line.trim();
|
|
638
|
+
if (!seenQuestionEcho && userPromptForFilter && t.includes(userPromptForFilter.slice(0, 15))) {
|
|
639
|
+
seenQuestionEcho = true;
|
|
640
|
+
} else if (seenQuestionEcho && t === '>') {
|
|
641
|
+
responseComplete = true;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (responseComplete) {
|
|
647
|
+
responseIdleTimer = setTimeout(() => {
|
|
648
|
+
process.stderr.write(`[agy-companion] Response complete.\n`);
|
|
649
|
+
deliverResponse();
|
|
650
|
+
}, RESPONSE_DONE_IDLE_MS);
|
|
651
|
+
} else {
|
|
652
|
+
responseIdleTimer = setTimeout(() => {
|
|
653
|
+
process.stderr.write(`[agy-companion] Response idle timeout.\n`);
|
|
654
|
+
deliverResponse();
|
|
655
|
+
}, RESPONSE_IDLE_MS);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
ptyProc.onExit(({ exitCode }) => {
|
|
661
|
+
if (!finished) {
|
|
662
|
+
if (questionSent) {
|
|
663
|
+
deliverResponse();
|
|
664
|
+
} else {
|
|
665
|
+
process.stderr.write('[agy-companion] agy exited before init completed.\n');
|
|
666
|
+
shutdown(exitCode ?? 1);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|