companion-for-agy 1.2.0-alpha.2 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-06-07
4
+
5
+ ### Fixed (Bugsweep)
6
+ - **Security:** Stale temp workspace from crashed run with same PID could leak permissions to new run — now cleaned on startup (`e8c5230`)
7
+ - Temp directory leak in sandbox/skip-permissions modes when no custom rules are set (`d406299`)
8
+ - Temp cleanup race on Windows: post-kill delay + rmSync retries for CWD lock (`41412d6`)
9
+ - ConPTY text extraction: stale cursor position, bold SGR false-positive, dedup scope too narrow (`c2194bb`)
10
+ - isNoiseLine false positives for blockquotes (`>`) and lines containing "tokens" keyword (`f6a8e7b`)
11
+
3
12
  ## [1.2.0-alpha.2] - 2026-06-07
4
13
 
5
14
  ### Changed
package/README_de.md CHANGED
@@ -41,6 +41,18 @@ npm install -g companion-for-agy
41
41
  - **macOS:** `xcode-select --install`
42
42
  - **Linux:** `sudo apt install build-essential python3` (Debian/Ubuntu)
43
43
 
44
+ ### Fehlerbehebung bei `node-pty` Build-Fehlern
45
+
46
+ Falls `npm install` mit nativen Kompilierungsfehlern fehlschlägt:
47
+
48
+ ```bash
49
+ # Alle Plattformen: native Module neu bauen
50
+ npm rebuild node-pty
51
+
52
+ # Windows: falls cl.exe nicht gefunden wird, Visual Studio Build Tools installieren
53
+ # dann "Developer Command Prompt" oder "x64 Native Tools" verwenden
54
+ ```
55
+
44
56
  ## Verwendung
45
57
 
46
58
  ```bash
@@ -73,6 +85,13 @@ companion-for-agy [optionen] "prompt"
73
85
  | `--json` | Ausgabe als JSON-Objekt |
74
86
  | `--debug` | Rohen PTY-Output in `agy-debug.log` speichern |
75
87
 
88
+ ### Umgebungsvariablen
89
+
90
+ | Variable | Beschreibung |
91
+ |----------|-------------|
92
+ | `AGY_COMPANION_AGY_PATH` | Pfad zur agy-Binary (wird automatisch erkannt wenn nicht gesetzt) |
93
+ | `AGY_PATH` | Alternativer Pfad zur agy-Binary |
94
+
76
95
  ### Beispiele
77
96
 
78
97
  ```bash
@@ -85,6 +104,9 @@ companion-for-agy --no-tools "Überprüfe diesen Code: ..."
85
104
  # Web-Recherche
86
105
  companion-for-agy --researcher "Neueste Infos zu Node.js 24"
87
106
 
107
+ # Nur-Lesen mit zusätzlicher Git-Berechtigung
108
+ companion-for-agy --read-only --allow "command(git log)" "prompt"
109
+
88
110
  # JSON-Output für programmatische Nutzung
89
111
  companion-for-agy --json --model gemini-3.5-pro "prompt"
90
112
  ```
@@ -121,6 +143,10 @@ companion-for-agy --json --model gemini-3.5-pro "prompt"
121
143
  - **CI/CD-Pipelines:** Automatisierte Gemini-Abfragen in Build-Scripts
122
144
  - **Scripting:** Jedes Szenario wo agys Antwort als Text benötigt wird
123
145
 
146
+ ## Hintergrund
147
+
148
+ Dieses Tool entstand, weil drei CLI-Agenten — **Claude Code**, **Codex** und **agy** — sich gegenseitig als Fallback-Berater aufrufen können müssen. Claude → Codex und agy → Claude/Codex funktionieren bereits, aber Claude → agy war durch den TUI-stdout-Bug blockiert.
149
+
124
150
  ## Lizenz
125
151
 
126
152
  MIT
package/ROADMAP.md CHANGED
@@ -63,6 +63,15 @@ Emit response tokens as they arrive (line-by-line or chunk-by-chunk) instead of
63
63
  ### Response Format Detection
64
64
  Detect whether agy's response is Markdown, JSON, or plain text and expose this in the JSON output (`"format": "markdown"`).
65
65
 
66
+ ### Robustness Improvements (from Bugsweep 2026-06-07)
67
+
68
+ Items identified during the systematic bug sweep that are design improvements, not defects:
69
+
70
+ - **Response idle timer minimum-progress threshold:** Currently, any single byte within the idle window resets the timer. A very slow stream (1 char/10s) keeps the timer alive indefinitely — only the global timeout catches it. Add a "minimum bytes since last check" threshold.
71
+ - **Signal handling for external kill:** Register `process.on('SIGTERM')` and `process.on('SIGINT')` to ensure temp workspace cleanup when the process is killed externally (e.g., by a parent orchestrator or Ctrl+C in a pipeline).
72
+ - **Dead code cleanup:** `tempSettingsCreated` variable is set but never read. Cleanup works unconditionally via `cleanupTemp()`.
73
+ - **Prompt-echo filter edge case:** Very short prompts (≤2 chars) identical to the response text are incorrectly filtered as prompt echoes. Rare in practice (requires the user's question to be the same as the answer), but theoretically possible.
74
+
66
75
  ## Completed (v1.2.0-alpha.1)
67
76
 
68
77
  - Trust dialog auto-confirmation (5-phase state machine)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "companion-for-agy",
3
- "version": "1.2.0-alpha.2",
3
+ "version": "1.2.0",
4
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
5
  "type": "module",
6
6
  "main": "src/agy-companion.mjs",
@@ -71,6 +71,38 @@ export function findAgyPath() {
71
71
 
72
72
  export const AGY_PATH = findAgyPath();
73
73
 
74
+ // ---------- Go-style Duration Parser ----------
75
+
76
+ export function parseDurationToMs(str) {
77
+ if (!str) return null;
78
+ const regex = /(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g;
79
+ let match;
80
+ let totalMs = 0;
81
+ let hasMatch = false;
82
+ while ((match = regex.exec(str)) !== null) {
83
+ hasMatch = true;
84
+ const value = parseFloat(match[1]);
85
+ const unit = match[2];
86
+ switch (unit) {
87
+ case 'ns': totalMs += value / 1e6; break;
88
+ case 'us':
89
+ case 'µs': totalMs += value / 1000; break;
90
+ case 'ms': totalMs += value; break;
91
+ case 's': totalMs += value * 1000; break;
92
+ case 'm': totalMs += value * 60000; break;
93
+ case 'h': totalMs += value * 3600000; break;
94
+ }
95
+ }
96
+ if (!hasMatch) {
97
+ const plain = parseFloat(str);
98
+ if (!isNaN(plain) && plain > 0) {
99
+ return Math.round(plain * 1000);
100
+ }
101
+ return null;
102
+ }
103
+ return Math.round(totalMs);
104
+ }
105
+
74
106
  // ---------- Permission-Presets ----------
75
107
 
76
108
  export const PERMISSION_PRESETS = {
@@ -105,6 +137,7 @@ export const PERMISSION_PRESETS = {
105
137
  // ---------- State-Machine-Patterns ----------
106
138
 
107
139
  export const TRUST_DIALOG_PATTERN = /Do you trust/;
140
+ export const LOGIN_PROMPT_PATTERN = /Select login method/;
108
141
  export const BANNER_MODEL_PATTERN = /Gemini \d[\d.]* \w+(?:\s*\([^)]*\))?/;
109
142
 
110
143
  export const STARTUP_DONE_PATTERNS = [
@@ -151,9 +184,9 @@ export function isNoiseLine(line, promptFilter = '') {
151
184
  const t = line.trim();
152
185
  if (!t) return true;
153
186
  if (/^[│┌└┐┘├┤┬┴┼─═╔╗╚╝╠╣╦╩╬▸►◉●▲▼◆□■╭╮╯╰]+$/.test(t)) return true;
154
- if (t.startsWith('>')) return true;
187
+ if (t === '>') return true;
155
188
  if (/[⣾⣷⣯⣟⡿⢿⣻⣽⠿⠾⠽⠼⠻⠺⠹⠸⠷⠶⠵⠴⠳⠲⠱⠰]/.test(t)) return true;
156
- if (/Generating|esc to cancel|for shortcuts|tokens/i.test(t)) return true;
189
+ if (/Generating|esc to cancel|for shortcuts/i.test(t)) return true;
157
190
  if (t === '?' || t === '? for shortcuts') return true;
158
191
  if (/^Gemini \d/.test(t)) return true;
159
192
  if (/^\d+\s*tokens$/.test(t)) return true;
@@ -172,6 +205,12 @@ export function extractByResponseColor(rawSection) {
172
205
  let inResponseColor = false;
173
206
  let pos = 0;
174
207
  const src = rawSection;
208
+ let hadCursorPos = false;
209
+ let cursorRow = null;
210
+ let cursorCol = null;
211
+ let gapHadNewline = false;
212
+ let currentGapNewline = false;
213
+ let preColorSpaces = 0;
175
214
 
176
215
  while (pos < src.length) {
177
216
  if (src[pos] === '\x1b') {
@@ -182,8 +221,26 @@ export function extractByResponseColor(rawSection) {
182
221
  const params = src.slice(pos + 2, end);
183
222
  if (params === '38;2;232;234;237' && cmd === 'm') {
184
223
  inResponseColor = true;
224
+ currentGapNewline = gapHadNewline;
225
+ hadCursorPos = false;
226
+ cursorRow = null;
227
+ cursorCol = null;
228
+ gapHadNewline = false;
185
229
  } else if (cmd === 'm') {
186
- inResponseColor = false;
230
+ if (params === '' || params === '0' || params === '39' ||
231
+ (params.startsWith('38;') && params !== '38;2;232;234;237') ||
232
+ /^3[0-7]$/.test(params) || /^9[0-7]$/.test(params)) {
233
+ inResponseColor = false;
234
+ }
235
+ } else if (inResponseColor && (cmd === 'H' || cmd === 'f')) {
236
+ hadCursorPos = true;
237
+ const parts = params.split(';');
238
+ if (parts.length >= 2) {
239
+ const r = parseInt(parts[0], 10);
240
+ const c = parseInt(parts[1], 10);
241
+ if (!isNaN(r)) cursorRow = r;
242
+ if (!isNaN(c)) cursorCol = c;
243
+ }
187
244
  }
188
245
  pos = end + 1;
189
246
  } else if (src[pos + 1] === ']') {
@@ -199,9 +256,29 @@ export function extractByResponseColor(rawSection) {
199
256
  while (textEnd < src.length && src[textEnd] !== '\x1b') textEnd++;
200
257
  const rawText = src.slice(pos, textEnd);
201
258
  const text = rawText.replace(/[\x00-\x08\x0b\x0e-\x1f\x7f]/g, '');
202
- if (text.length > 0) segments.push(text);
259
+ if (text.length > 0) {
260
+ segments.push({
261
+ text,
262
+ newLine: currentGapNewline && !hadCursorPos,
263
+ startCol: hadCursorPos ? cursorCol : null,
264
+ startRow: hadCursorPos ? cursorRow : null,
265
+ estimatedCol: !hadCursorPos ? preColorSpaces + 1 : null,
266
+ });
267
+ currentGapNewline = false;
268
+ if (hadCursorPos && cursorCol !== null) {
269
+ cursorCol += text.length;
270
+ }
271
+ }
203
272
  pos = textEnd;
204
273
  } else {
274
+ if (src[pos] === '\n') {
275
+ gapHadNewline = true;
276
+ preColorSpaces = 0;
277
+ } else if (src[pos] === ' ') {
278
+ preColorSpaces++;
279
+ } else {
280
+ preColorSpaces = 0;
281
+ }
205
282
  pos++;
206
283
  }
207
284
  }
@@ -209,10 +286,58 @@ export function extractByResponseColor(rawSection) {
209
286
  if (segments.length === 0) return null;
210
287
 
211
288
  const deduped = segments.filter((s, i) =>
212
- !segments.some((o, j) => j !== i && o.length > s.length && o.startsWith(s))
289
+ !segments.some((o, j) => j !== i &&
290
+ s.startCol === null &&
291
+ o.text.length > s.text.length && o.text.startsWith(s.text))
213
292
  );
214
293
 
215
- const combined = deduped.join('').trim();
294
+ let combined = '';
295
+ let lastEndCol = null;
296
+ let lastRow = null;
297
+ for (let i = 0; i < deduped.length; i++) {
298
+ const seg = deduped[i];
299
+ const rowChanged = seg.startRow !== null && lastRow !== null && seg.startRow !== lastRow;
300
+ if (i === 0) {
301
+ combined = seg.text;
302
+ if (seg.startCol !== null) {
303
+ lastEndCol = seg.startCol + seg.text.length;
304
+ } else if (seg.estimatedCol !== null) {
305
+ lastEndCol = seg.estimatedCol + seg.text.length;
306
+ }
307
+ if (seg.startRow !== null) lastRow = seg.startRow;
308
+ } else if (seg.startCol !== null && lastEndCol !== null && !rowChanged) {
309
+ const gap = seg.startCol - lastEndCol;
310
+ if (gap > 0) combined += ' '.repeat(gap);
311
+ else if (gap < 0) combined = combined.slice(0, Math.max(0, combined.length + gap));
312
+ combined += seg.text;
313
+ lastEndCol = seg.startCol + seg.text.length;
314
+ if (seg.startRow !== null) lastRow = seg.startRow;
315
+ } else if (seg.startCol !== null && !rowChanged) {
316
+ combined += seg.text;
317
+ lastEndCol = seg.startCol + seg.text.length;
318
+ if (seg.startRow !== null) lastRow = seg.startRow;
319
+ } else if (rowChanged || seg.newLine) {
320
+ if (seg.startCol !== null && lastEndCol !== null && seg.startCol === lastEndCol) {
321
+ combined += seg.text;
322
+ } else {
323
+ combined = combined.trimEnd() + ' ' + seg.text;
324
+ }
325
+ if (seg.startCol !== null) {
326
+ lastEndCol = seg.startCol + seg.text.length;
327
+ } else if (seg.estimatedCol !== null) {
328
+ lastEndCol = seg.estimatedCol + seg.text.length;
329
+ } else {
330
+ lastEndCol = null;
331
+ }
332
+ if (seg.startRow !== null) lastRow = seg.startRow;
333
+ } else {
334
+ combined += seg.text;
335
+ if (lastEndCol !== null) {
336
+ lastEndCol += seg.text.length;
337
+ }
338
+ }
339
+ }
340
+ combined = combined.replace(/ {2,}/g, ' ').trim();
216
341
  return combined.length > 0 ? combined : null;
217
342
  }
218
343
 
@@ -264,14 +389,16 @@ export function extractResponse(stripped, rawSection, promptFilter = '', effecti
264
389
  if (meaningful.length === 0) return null;
265
390
 
266
391
  let best = meaningful.join('\n').trim();
267
- if (promptFilter) {
268
- const promptWords = promptFilter.split(/\s+/).slice(0, 5).join('');
269
- best = best.split('\n')
270
- .filter(l => !l.replace(/\s/g, '').startsWith(promptWords.replace(/\s/g, '')))
271
- .join('\n')
272
- .trim();
392
+ const echoFilter = effectiveFilter || promptFilter;
393
+ if (echoFilter) {
394
+ const cleaned = stripPromptEcho(best, echoFilter);
395
+ if (cleaned !== null) best = cleaned || '';
273
396
  }
274
- return best || null;
397
+ if (promptFilter && promptFilter !== echoFilter) {
398
+ const cleaned = stripPromptEcho(best, promptFilter);
399
+ if (cleaned !== null) best = cleaned || '';
400
+ }
401
+ return best.trim() || null;
275
402
  }
276
403
 
277
404
  // ---------- CLI Main ----------
@@ -362,7 +489,7 @@ if (isMainModule()) {
362
489
  jsonOutput = true;
363
490
  } else if (arg === '--sandbox') {
364
491
  permissionMode = 'sandbox';
365
- } else if (arg === '--skip-permissions') {
492
+ } else if (arg === '--skip-permissions' || arg === '--dangerously-skip-permissions') {
366
493
  permissionMode = 'skip-permissions';
367
494
  } else if (arg === '--no-tools') {
368
495
  permissionMode = 'no-tools';
@@ -370,6 +497,16 @@ if (isMainModule()) {
370
497
  permissionMode = 'researcher';
371
498
  } else if (arg === '--read-only') {
372
499
  permissionMode = 'read-only';
500
+ } else if (arg === '--print-timeout' && rawArgs[i + 1]) {
501
+ const ms = parseDurationToMs(rawArgs[++i]);
502
+ if (ms !== null) timeoutMs = ms;
503
+ } else if (arg.startsWith('--print-timeout=')) {
504
+ const ms = parseDurationToMs(arg.slice(16));
505
+ if (ms !== null) timeoutMs = ms;
506
+ } else if (arg === '-p' || arg === '--print' || arg === '--prompt') {
507
+ // Ignored: agy-companion runs in interactive mode internally to capture PTY/ANSI
508
+ } else if (arg === '-i' || arg === '--prompt-interactive') {
509
+ // Ignored: agy-companion runs interactive by default
373
510
  } else if (arg === '--allow' && rawArgs[i + 1]) {
374
511
  customAllow.push(rawArgs[++i]);
375
512
  } else if (arg === '--deny' && rawArgs[i + 1]) {
@@ -396,6 +533,9 @@ if (isMainModule()) {
396
533
  const tempWorkspace = path.join(os.tmpdir(), `agy-companion-${process.pid}`);
397
534
  let tempSettingsCreated = false;
398
535
 
536
+ // Clean stale workspace from a previous crashed run with same PID
537
+ try { fs.rmSync(tempWorkspace, { recursive: true, force: true }); } catch (_) {}
538
+
399
539
  if (allAllow.length > 0 || allDeny.length > 0) {
400
540
  const geminiDir = path.join(tempWorkspace, '.gemini');
401
541
  fs.mkdirSync(geminiDir, { recursive: true });
@@ -491,7 +631,7 @@ if (isMainModule()) {
491
631
  cols: 220,
492
632
  rows: 50,
493
633
  cwd: tempWorkspace,
494
- env: { ...process.env, TERM: 'xterm-256color' },
634
+ env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor' },
495
635
  });
496
636
 
497
637
  let rawBuffer = '';
@@ -514,11 +654,7 @@ if (isMainModule()) {
514
654
 
515
655
  function cleanupTemp() {
516
656
  try {
517
- if (tempSettingsCreated) {
518
- fs.rmSync(tempWorkspace, { recursive: true, force: true });
519
- } else {
520
- fs.rmdirSync(tempWorkspace);
521
- }
657
+ fs.rmSync(tempWorkspace, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
522
658
  } catch (_) {}
523
659
  }
524
660
 
@@ -539,8 +675,11 @@ if (isMainModule()) {
539
675
  process.stderr.write(`[agy-companion] Debug log: ${debugPath}\n`);
540
676
  }
541
677
 
542
- cleanupTemp();
543
- process.exit(code);
678
+ // Windows holds a CWD lock on tempWorkspace until child processes fully terminate
679
+ setTimeout(() => {
680
+ cleanupTemp();
681
+ process.exit(code);
682
+ }, 1000);
544
683
  }, 500);
545
684
  }
546
685
 
@@ -599,6 +738,12 @@ if (isMainModule()) {
599
738
  }
600
739
  }
601
740
 
741
+ if (LOGIN_PROMPT_PATTERN.test(recentStripped)) {
742
+ process.stderr.write(`[agy-companion] agy is not signed in. Please run 'agy' manually and complete sign-in first.\n`);
743
+ shutdown(3);
744
+ return;
745
+ }
746
+
602
747
  if (!trustHandled && TRUST_DIALOG_PATTERN.test(recentStripped)) {
603
748
  trustHandled = true;
604
749
  process.stderr.write(`[agy-companion] Trust dialog detected. Auto-confirming...\n`);